|
56429
|
1967
|
28
|
2026-05-19T08:06:11.621779+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177971621_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1356238323261734672
|
-8996598804339809755
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
56428
|
NULL
|
NULL
|
NULL
|
|
56428
|
1967
|
27
|
2026-05-19T08:06:09.958178+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177969958_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6870583155641010293
|
-8132287983061324858
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
PhpStormViewNavigateCodeLaravelRefactorTOOISWindowFV faVsco.jsv?9 JY-20676-delete-report-related-objectsProiect vC) AutomatedReport.onp© AskAnythingPromptServiceTest.php© ActivityAskAnythinglest.phpe) -xooricontro||erRateLimit est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpC)FakeProphetClient.oho© AskAnythingPromptService.ph© BaseService.phpMidd lewareM RequestsSoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php() ListenerRoleCannotHaveAdminOrMa() RoleAttrTest.phpInteractionsInvitationsalobs" ListenerRoleCannotHaveAdminOrManagerPermissionRule.php© Activity/Close/Service.php©) Activity/RinqCentral/Service.phpListeners© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpD McpModelsD NotificationsD OrganizationSettingsD PlaylistsD Policies( RepositoriesC Rules0 SavedSearches@ SavedSearchCRUDFeatureTest.php› C Search> C ServiceProvidersfinal class SavedSearchCRUDFeatureTest extends TestCase194 D >255256257 D >314public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}#[\PHPUnit\Framework\Attributes\Test]public function userCanDeleteASavedSearch: void{...}Local ChandesConsole XLooXv Changes 18 files=.env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthinaPromptDto.php app/Component/AskAnvthina/Dtos© AskAnythingPromptService.php app/Component/AskAnything@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthina© Ask.JiminnvReportsController.pho app/Htto/Controllers/API/V2T.O©® Side-by-side viewerCurrent version raw sal querv.sallDo not ignoreHighlight words ?SELECT activities. *C) Constants.ono apo/Comoonent/SClM© CoreUser.php app/DTO/SCIM/AAD/ResponseC) CoreUserRequest.ono aoo/DTO/SCIMIAAD/Requesi© JiminnyDebugCommand.php app/Console/CommandsC ListenerRoleCannotHaveAdminOrManagerPermissionRule.php app/Rules@* ListenerRoleCannotHaveAdminOrManagerPermissionRuleTest.ohn tests/Unit/FFROM activitiesINNER JOIN activity_processing_states AS aps ON aps.activity_id = activities.idINNER JOIN users ON users.id = activities.user_idWHERE activities.is_internal = 0AND users.status = 1AND activities.type NOT IN ('email_inbound', 'email_outbound',AND aps.state = 'send_processing_notificationsAND aps.status IN ('finished', 'failed')AND aps.created_at BETWEEN ? AND ?AND aps.deleted_at IS NULL-- Optional team filter (add when teamId is provided):-- AND users.team id = ?php logging.php config© RoleAttr.php app/Component/SCIM/Mutators/Attributes/Usec) RoleAttrTest.nhn tests/Unit/Comnonent/SCIM/Mutators/Attributes/Use@ ScimProvisioning.php app/Component/SCIM© Search.php app/Models/ActivityUnversioned Files 9 filesE.env.nikilocal app=.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/RE favicon.ico publicEids.txt appT raw sal querv.sal apn© SimulateWebhooksCommand.php app/Console/Commands/Crm/HubspotMJ WEBHOOK FILTERING IMPLEMENTATION.md annThe Hunspell plugin has been deprecated. If you're not writing in Hungarian, you can safely uninstall Hunspell without affecting the dicries for other languages. (today 10:00)"suppont Dally • In 3n 04m100% 5• Tue 19 May 11:06:09U AskJiminnyReportActivityServiceTest v+0 ..= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~29 6t34 6t >A3416 ^ v45 6t- 119- 131135136 >public function construct(UserRepository SuserRepository){...}public function shouldApplyQueries: bool{...}public function getQueries: FilterDefinitionQueryCollection{...}ouolic function toArravo: arrav...?1 usaadprivate function getOptions: arrayt...public function getValue: arrayt...lusageprivate function getDefaultValue: arrayt...public function getValidationRules(?string $prefix = null): array{...}CascadeAsk Jiminny Report Mmocd → null, "status → Talsel3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['ask_anything_prompt_id' = null, 'status' = falsel ).Per your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.8 files 400-7 )Ask anvthina (4D1+ <> Code Claude Opus 4.7 Medium* Reject allAccept allW Windsurf Teams 314:1 UTF-8 P 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56427
|
1966
|
22
|
2026-05-19T08:06:10.575002+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177970575_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6509625738266471330
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56426
|
1966
|
21
|
2026-05-19T08:06:09.913283+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177969913_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2078138591382873457
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
56423
|
NULL
|
NULL
|
NULL
|
|
56425
|
1967
|
26
|
2026-05-19T08:06:08.920287+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177968920_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1356238323261734672
|
-8996598804339809755
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
56424
|
NULL
|
NULL
|
NULL
|
|
56424
|
1967
|
25
|
2026-05-19T08:06:05.812954+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177965812_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-351575931525056321
|
-8132358351805502526
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
PhostormProiectVIewINavicareCodeLaravelKeractorTOOISWindowFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpA console [STAGING)© CoachingFeedbackCoachUserln.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd eware• → Requests© AskAnythingPromptService.phg© BaseService.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~© SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.phppubuic tunccion conscruccuserkeposicory suserkeposicorw...© CoreUser.phd() ListenerRoleCannotHaveAdminOrMa() RoleAttrTest.php29 6tpublic function shouldApplyQueries: bool{...}InvitationsIHlohsIn ListenersD McpN ModelsiM NotificationsD OrganizationSettingsD PlaylistsD Policies( RepositoriesC RulesC SavedSearches@ SavedSearchCRUDFeatureTest.php› C Search›_ serviceProvidersListenerRoleCannotHaveAdminOrManagerPermissionRule.php© Activity/Close/Service.php©) Activity/RinqCentral/Service.php34 6t >public function getQueries: FilterDefinition0ueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3416 ^ v45 6touolic function toArravo: arrav..."194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}1 usaad#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions: arrayt...257 D >314118public function getValue: arrayt...130=131135136 >lusageprivate function getDefaultValue: arrayt...public function getValidationRules(?string $prefix = null): array{...}Console XLooX©O ActivityController.php app/Htte/Controllers/AP© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthinaPromptDto.php app/Component/AskAnvthina/Dtos© AskAnvthinaPromptService.php app/Component/AskAnvthing@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthina© Ask.JiminnvReportsController.pho app/Htto/Controllers/API/V2T.OCurrent version app/Rules/Lis<2ohnDo not ignoreHighlight words ?declanp ctrict +vnec=1)+namesnace Liminnv Rules.C) Constants.ono apo/Comoonent/SClMI© CoreUser.php app/DTO/SCIM/AAD/ResponseC) CoreUserRequest.ono aoo/DTO/SCIMIAAD/[EMAIL] app/Console/Commandsphp logging.php configScimProvisioning.php app/Component/SCIM(C) Search.oho aon/Models/Activitviuse Closure:use Illuminate \Contracts \Validation ValidationRule:use ILLuminace support collecclonuse Illuminate\Translation\PotentiallyTranslatedStringuse Jiminny nodels kole'Unversioned Files 13 filesuse Jiminny nodels user.env.nikilocal appE .env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/RE favicon.ico publicE ids.txt app* ListenerRoleCannotHaveAdminOrManagerPermissionRule.php app/RulesQit LictonerPoleCannotHaveAdminOrManaqerPermissionRuleTest.php tests/Ta raw sal_query.sal app© RoleAttr.php app/Component/SCIM/Mutators/Attributes/Use°) RoleAttrTest.php tests/Unit/Co© SimulateWebhooksCommand.php app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING_IMPLEMENTATION.md apdfinal readonly class ListenerRoleCannotHaveAdmin0rManagerPermissionRule implements ValidationRulepublic function construct(/** @var Collection<Role> */orivate collection sroles.* @param int[] $valve* Gparam Closure(strina): PotentialutranstatedStrina Sfoalpublic function validate(string $attribute, mixed $value, Closure $fail): voidThe Hunspell plugin has been deprecated. If you're not writing in Hungarian, you can safely uninstall Hunspell without affecting the dictionaries for other languages. (today 10:00)"suppont Dally • In 3n 04m100% 5• Tue 19 May 11:06:05U AskJiminnyReportActivityServiceTest v+0 ..CascadeAsk Jiminny Report Mmocd → null, "status → Talsel3. loggle validationrtsController.php: 126-169 - In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload (['ask_anything_prompt_id' = null, 'status' = falsel ).Per your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.odl "* Reject allAccept all8 files 400-7 )Ask anvthing (*4D+ <> Code Claude Opus 4.7 MediumW Windsurf Teams 314:1 UTF-8 P 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56423
|
1966
|
20
|
2026-05-19T08:06:05.821541+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177965821_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"}]...
|
3224510618648346242
|
-8132287982927107134
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily • in 3h 54 m100% CTue 19 May 11:06:05QDescribe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56422
|
1967
|
24
|
2026-05-19T08:06:02.892711+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177962892_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8334616115047382362
|
-9140749176787754460
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error...
|
56421
|
NULL
|
NULL
|
NULL
|
|
56421
|
1967
|
23
|
2026-05-19T08:06:02.304679+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177962304_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
6509625738266471330
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56420
|
1967
|
22
|
2026-05-19T08:05:59.734955+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177959734_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
-1655837267692718254
|
-9140731314152200412
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
56418
|
NULL
|
NULL
|
NULL
|
|
56419
|
1966
|
19
|
2026-05-19T08:05:59.773080+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177959773_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6509625738266471330
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
56417
|
NULL
|
NULL
|
NULL
|
|
56418
|
1967
|
21
|
2026-05-19T08:05:59.120780+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177959120_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1356238323261734672
|
-8996598804339809755
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56417
|
1966
|
18
|
2026-05-19T08:05:59.104802+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177959104_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1655837267692718254
|
-9140731314152200412
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56416
|
1967
|
20
|
2026-05-19T08:05:55.730221+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177955730_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2895291531155995682
|
-8635651002824405565
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
PhostormProiectVIewINavicareCodeLaravelKeractorFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd eware40) SoftPhoneManager.phpRequestsInteractionsa InvitationsIHlobsIn ListenersD McpN Modelsi194 D >M Notificationsm oraanizationsettingsW rlayllstsD Policies257 D >314( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search_ serviceProvidersSheliConsole XLoo XChanges 14 Tiles= .env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPrompt.php app/Models/AskAnythingc) AsKAnvihinaPromotbto.ono apo/comoonent/AskAnvihino/Dros© AskAnvthinaPromptService.php app/Component/AskAnvthingAsKAnywingPromptuto.pnpA console [STAGING]© CoachingFeedbackCoachUserln.php X© BaseService.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~C) RoleAttr.png() ListenerRoleCar© ScimProvisioning.php() RoleAttrTest.phppubLic tunccion conscructuserkeposicory suserkeposicorvor...29 6tpublic function shouldApplyQueries: boolf...}ListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php(C) Activity/RinqCentral/Service.php34 6t >public function getQueries: FilterDefinitionQueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3416 ^ v45 6tpubLic function toArray): arrayl...лpublic function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}1 usaad#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions: arrayt...=119-130=131136 >public function getValue: arrayt...lusageprivate function getDefaultValue: arrayt...Ask anvthina (4D1public function getValidationRules(?string $prefix = null): array{...}4 @s Codo Chudo Onuc 17 Modium+ → @ Side-by-side viewer -Do not ignoreHighlight words 158 5604af40 app/CompCurrent versionuse Jiminny Models Role:use liminnv Model c Team:use Jiminny Models Role;use liminnv Model c TeamC)AskAnvthinaPromotServiceTlest.oho tests/Unit/Comoonent/AskAnvthinause Jiminny Models\User;use liminnv Senvices IlsenSenvice.use Jiminny Models\User;use Jiminny\Services\UserService:C)AskJiminnvRevortsController.ono apo/Htto/Controlllers/APVN2use Sentry:use Jiminny\Contracts\Acl \RoleRepositoryInterface:C) Constants.ono apo/Comoonent/SClMI@ CoreUser.php app/DTO/SCIMIAAD/Responseuse ulluminate\Support\Facades\Validaton.class ScimProvisioninguse Jiminny Rules\DependentRolesRule:C) CoreUserRequest.ono ano/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandsphp loadina.nho confialCScimProvisionina.ohn aon/Comnonent/SCINuse Jiminny Rules\ListenerRoleCannotHaveAdmin0rManagerPermissionRule:public function _construct(private readonly Dispatcher SeventDispatcheruse Jiminny Rules \ListenerRoleRequiresListenerFeatureRuleuse Jiminny Actions UpdateUserRolesAction:orivace readonly userservice suserserviceuse Jiminny DTO\Roles UserRolesSyncDT0:(C) Search.oho aon/Models/Activitvlprivate readonly RoleChangeLogger SroleChangeLoggeruse Sentry:Unversioned Files 13 filesclass Sc1mProv1s10n1nqlpublic function validateUserIdInput(Sid. Team Steam): boolf'email' => $dto->getEmailPrimaryWorkO],SuserUodateDatalpublic function -_constructorivate readonly Daspatcher seventiisoatcher.private readonly UserService $userService,orivate readonly RolechangeLogger SroleChangeLogger..orivate readonly RoleRepositoryInterface $roleRepository,orivate readoniv UodatelserRolesAction SundatelserRolesAction.if (! SnewUser->roles()->exists()) {Snewliser->addRole User: ROLE ANALYSTO:/** @var Role Srole */Snole = Role• •quenvo->whenedr'namel => Isen: •ROI F ANAI VSTID->finctinCailo•nublic function validatellsenTdinnut(Sid. Team Steam• hoollThe Huncnell nluain hac haonout affectina the dictionarios for other lanquades (todav 10-00)• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:55AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.* Reject allAccent all•01.4 difterencesWN Windsurf Toams 214-1UTF.8ih 4 spaces...
|
56414
|
NULL
|
NULL
|
NULL
|
|
56415
|
1966
|
17
|
2026-05-19T08:05:55.753410+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177955753_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3921796768863820486
|
-8634233732034220862
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% C3• Tue 19 May 11:05:55QDescribe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A® 4Message #the_people_of_jiminny+...
|
56412
|
NULL
|
NULL
|
NULL
|
|
56414
|
1967
|
19
|
2026-05-19T08:05:55.077920+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177955077_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
6509625738266471330
|
-8996616125002602716
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56413
|
1967
|
18
|
2026-05-19T08:05:52.564490+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177952564_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIeWINavicareCodeLaravelWindowFV faVsco.js PhostormVIeWINavicareCodeLaravelWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect(C) AutomatedReport.onp© AskAnythingPromptServiceTest.php- ActivtvaskAnvihind est.ono'c SavedSearchCRUDreaturelest.onp ~© AskAnythingPromptDto.phpe) -xooricontro||erRateLimit est.onoC)FakeProphetClient.oho© AskAnythingPromptService.php© BaseService.php> _ Midd eware4) SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php• _ Requests)© CoreUser.phd() ListenerRoleCar() RoleAttrTest.phpInteractionsInvitationsListenerRoleCannotHaveAdminOrManagerPermissionRule.phpActivity/Close/Service.php©) Activity/RinqCentral/Service.phpIHlobsIn Listeners© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpD Mcpfinal class SavedSearchCRUDFeatureTest extends TestCase194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsD OrganizationSettingsW rlayllsts257 D >#[\PHPUnit\Framework\Attributes\Test]public function userCanDeleteASavedSearch: void{...}_ Pollcles( RepositoriesM PulosC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search0 ServiceProvidersLocal ChandesConsole XLooX= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING)© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinim29 đ:34 6t >A3 A16 ^ v45 6t>118119-136135136 €t>public function construct(UserRepository SuserRepository){...}public function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...}public function toArrav@): arravs..?1usaaeprivate function getOptions(): array(...}public function getValue(): arrayf...1 usageprivate function getDefaultValue(): arrayf...}public function getValidationRules(?string $prefix = null): arrayf...}"suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:52AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..mocd → null, "status → Talsel3. loggle validationenabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.8 files 400-7 )Ask anvthing (*4D+ <> Code Claude Opus 4.7 Medium* Reject allAccept allNothina to chowaries for other languages. (today 10:00)W Windsurf Teams 314:1 UTF-8 P 4 spaces...
|
NULL
|
-3920068525392978573
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIeWINavicareCodeLaravelWindowFV faVsco.js PhostormVIeWINavicareCodeLaravelWindowFV faVsco.js°9 JY-20676-delete-report-related-objectsProiect(C) AutomatedReport.onp© AskAnythingPromptServiceTest.php- ActivtvaskAnvihind est.ono'c SavedSearchCRUDreaturelest.onp ~© AskAnythingPromptDto.phpe) -xooricontro||erRateLimit est.onoC)FakeProphetClient.oho© AskAnythingPromptService.php© BaseService.php> _ Midd eware4) SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php• _ Requests)© CoreUser.phd() ListenerRoleCar() RoleAttrTest.phpInteractionsInvitationsListenerRoleCannotHaveAdminOrManagerPermissionRule.phpActivity/Close/Service.php©) Activity/RinqCentral/Service.phpIHlobsIn Listeners© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpD Mcpfinal class SavedSearchCRUDFeatureTest extends TestCase194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsD OrganizationSettingsW rlayllsts257 D >#[\PHPUnit\Framework\Attributes\Test]public function userCanDeleteASavedSearch: void{...}_ Pollcles( RepositoriesM PulosC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search0 ServiceProvidersLocal ChandesConsole XLooX= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING)© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinim29 đ:34 6t >A3 A16 ^ v45 6t>118119-136135136 €t>public function construct(UserRepository SuserRepository){...}public function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...}public function toArrav@): arravs..?1usaaeprivate function getOptions(): array(...}public function getValue(): arrayf...1 usageprivate function getDefaultValue(): arrayf...}public function getValidationRules(?string $prefix = null): arrayf...}"suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:52AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..mocd → null, "status → Talsel3. loggle validationenabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.8 files 400-7 )Ask anvthing (*4D+ <> Code Claude Opus 4.7 Medium* Reject allAccept allNothina to chowaries for other languages. (today 10:00)W Windsurf Teams 314:1 UTF-8 P 4 spaces...
|
56410
|
NULL
|
NULL
|
NULL
|
|
56412
|
1966
|
16
|
2026-05-19T08:05:54.356663+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177954356_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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}]...
|
6974494513498781598
|
-8996598254583995868
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56411
|
1966
|
15
|
2026-05-19T08:05:52.569306+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177952569_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% CTue 19 May 11:05:52Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
892387482773252379
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% CTue 19 May 11:05:52Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
56409
|
NULL
|
NULL
|
NULL
|
|
56407
|
1967
|
15
|
2026-05-19T08:05:29.690839+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177929690_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8334616115047382362
|
-9140749176787754460
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56406
|
1966
|
13
|
2026-05-19T08:05:29.716379+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177929716_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6509625738266471330
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
56403
|
NULL
|
NULL
|
NULL
|
|
56405
|
1967
|
14
|
2026-05-19T08:05:28.784727+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177928784_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
6509625738266471330
|
-8996616125002602716
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
56404
|
NULL
|
NULL
|
NULL
|
|
56404
|
1967
|
13
|
2026-05-19T08:05:25.774950+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177925774_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5803414936471392232
|
-8636765907853005370
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
PhostormProiect vVIewINavicareCodeKeractorTOOISWindowFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php XA console [STAGING]© CoachingFeedbackCoachUserln.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd ewareRequestsInteractionsInvitationsIHlobsIn ListenersD McpN ModelsiM Notificationsm oraanizationsettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search›_ serviceProvidersC SoftPhoneManager.php() ListenerRoleCannotHaveAdminOrMa© AskAnythingPromptServiceTest.php© AskAnythingPromptDto.php© BaseService.phpC) RoleAttr.png© ScimProvisioning.php() RoleAttrTest.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~29 6tpublic function shouldApplyQueries: boolf...}ListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.php34 6t >public function getQueries: FilterDefinitionQueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3416 ^ v45 6touolic function toArravo: arrav...?194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}1 usage257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions: arrayt...=119=130=131136 >public function getValue: arrayt...lusageprivate function getDefaultValue: arrayt...Ask anvthina (4D1public function getValidationRules(?string $prefix = null): array{...}4 @s Codo Chudo Onuc 17 ModiumLocal ChandesConsole XLooX• Changes 16 files.E .env.local appActivityController.php app/Http/Controllers/API© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthingPromptDto.pho app/Component/AskAnvthina/Dtos© AskAnvthinaPromptService.php app/Component/AskAnvthinaSide-bv-side viewerDo not ignore8 5604af40 app/Services/Telephony/TextMessagingService.phpXIIKLOCurrent versionuse Jiminny Models PlavbookCatedory:use liminnv Model c Team:use Jiminny Models\User;use liminnv Senvicos ActivitvSenvico•use Jiminny Models\PlaybookCategory:use liminnv Model c Toam•use Jiminny Models\User;use liminnv Senvices ActivitvSenvice.© AskAnythingPromptServiceTest.php tests/Unit/Component/AskAnything© AskJiminnvReportsController.phv app/Htto/Controllers/API/N2use Jiminny\Services\Crm\CachedCrmServiceDecorator:use Jiminny\Services \Activity\Crm0wnerResolver:(C) Constants.ono aoo/Comoonent/SClM@ CoreUser.pho app/DTO/SCIMIAAD/Responseuse Jiminny\Services\Crm \ProviderRegistry:use Jiminny\Services\Crm\CachedCrmServiceDecorator:use Twilio\Exceptions\ConfigurationException;use Jiminny|Services\Crm\ProviderRegistry:(C) CoreUserRequest.ono ano/DTO/SCIMIAAD/Requestuse Twilio\Exceptions\TwilioException:use Twilio\Exceptions\ConfigurationException;@.liminnvDebuaCommand.ohn apo/Console/Commandsuse Twilio Exceptions \TwilioException:php loadina.nho confiaA Lliminnvl Servicae Telenhonv > TeytMeccaainaService > buildA ctivitv/)©ScimProvisioning.php app/Component/SCIMSteam = Suser->qetTeamO:(C) Search.nho ano/Models/ActivitviSteam = suser->qetteamor@ SoftPhoneManader.nhn app/Comnonent/Twilio/Conference/ConferenceMan:try© TextMessagingService.php app/Services/Telephony• UInvorcionod Ciloc 12 filodScrmService = Sth1s->prov1derReq1stry->qet(Steam->crm->provider)try{ScrmResolver = app(CrmownerResolver::class.if (! Suser->isCrmRequired@) {ScrmService->setuser(steam->cerowneron:}else {anteorationAdman' => suser'providerSlug' => $team->getCrmConfiguration()->getProviderName(),ScrmService->setUser(Suser)if (Scustomerid)$catch Throwable Sthrowabille) 4ScrmService = Sthis->providerReaistrv->aet(Steam->crm->provider).Ouery the CRM for full details of this customerIsleadl Saccount Sonnontunitv. Scontact. Sctaae. SermCountrvlodel =SernSenvice-snanceReconds(ScustomenTd.SohiectTvne)-if@ Suser->isCrmReauiredon<SennSenvice-scetlicen(Steam->aetOwnerd)} else {afely uninstall Hunspell without affecting the dictionaries for other languages. (today 10:00)• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:25AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the search* Reject allAccent alli•01.6 ditterencesWN Windsurf Teamsio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56403
|
1966
|
12
|
2026-05-19T08:05:25.106379+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177925106_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:25Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
6985653923973758564
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:25Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56402
|
1967
|
12
|
2026-05-19T08:05:22.184773+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177922184_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavicareCodeKeractorlooIsFV faVsco.js?9 J PhostormINavicareCodeKeractorlooIsFV faVsco.js?9 JY-20676-delete-report-related-objectsProiectC) AutomatedReport.onp© AskAnythingPromptServiceTest.php- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv© BaseService.php> _ Midd ewareC SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.phpRequests() ListenerRoleCanr() RoleAttrTest.phpInteractionsa InvitationsListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.phpIHlohsIn Listeners© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpD McpN Modelsifinal class SavedSearchCRUDFeatureTest extends TestCase194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsOrganizationSettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}›_ serviceProvidersLocal ChangesConsole XLoo Xv Changes 16 files+ → @ Side-by-side viewer -Do not ignoreHighlight words 158 5604af40 app/Comp© ActivityController.php app/Http/Controllers/APIc)AsKAnythingPrompt.php app/Models/AsKAnythinguse Jiminny Models Role:c) ASKAnvihinaPromotbto,ono apo/comoonent/AskAnvihino/Dros© AskAnythingPromptService.php app/Component/AskAnythingC)AskAnvthinaPromotServiceTest.oho tests/Unit/Comoonent/AskAnvthinal© AskJiminnvReportsController.php app/Htto/Controllers/API/V2use liminnv Model c Team:use Jiminny Models\User;use liminnv Senvices IlsenSenvice.use Sentry:C) Constants.ono aoo/Comoonent/SClMI@ CoreUser.pho app/DTO/SCIMIAAD/Responseclass ScimProvisioningC) CoreUserRequest.ono ano/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandspublic function _construct(private readonly Dispatcher SeventDispatcherphp logging.php config@ ScimProvisionina.phnapn/Comnonent/SCIMorivace readonly userservice suserserviceC) Search.nho aon/Models/Activitvprivate readonly RoleChangeLogger SroleChangeLogger@ SoftPhoneManader.nhn ann/Comnonent/Twilio/Conference/ConferenceManad© TextMessagingService.php app/Services/Telephony• LInvorcionod Ciloc 12 filocpublic function validateUserIdInput(Sid. Team Steam): boolT'email' => Sdto->qetEmailPrimaryWorkO],SuserUodateDatalif (! SnewUser->roles()->exists()) {Snewliser->addRole User: ROLE ANALYSTO:/** @var Role Srole */Snole = Role• •quenvo->whenedr'namel => Isen: •ROI F ANAI VSTID->finctinCailo•e Hunsnell nluain hac heonolv uninctall Huncnoll without affectina the dictionarios for other lanquadec (todav 10-00)= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~pubLic tunccion conscruct userkeposicory suserkeposicorw?...29 6tpublic function shouldApplyQueries: bool{...}34 ot>public function getQueries: FilterDefinitionQueryCollection{...}A3416 ^ v45 6tpubLic function toArrayo): arrayi...h1 usaadprivate function getOptions: arrayt...=119=130public function getValue: arrayt...=131lusageprivate function getDefaultValue: arrayt...136 >public function getValidationRules(?string $prefix = null): array{...}Current versionuse Jiminny Models Role;use liminnv Model c Teamuse Jiminny Models\User;luse liminnv Senvices IlcenSenvico•use Jiminny\Contracts\Acl \RoleRepositoryInterface:use Illuminate\Support\Facades|VaLidaton.use Jiminny Rules\DependentRolesRule;use Jiminny Rules\ListenerRoleCannotHaveAdmin0rManagerPermissionRule:use Jiminny Rules \ListenerRoleRequiresListenerFeatureRuleuse Jiminny Actions UpdateUserRolesAction:use Jiminny DTO\Roles UserRolesSyncDT0:use Sentry:class Sc1mProv1s10n1nqlpublic function -_constructorivate readonly Daspatcher seventiisoatcher.private readonly UserService $userService,orivate readonly RolechangeLogger SroleChangeLogger..orivate readonly RoleRepositoryInterface $roleRepository,orivate readoniv UodatelserRolesAction SundatelserRolesAction.nublic function validatellsenTdinnut(Sid. Team Steam• hooll• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:21AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.* Reject allAccent alliAsk anvthina (4D14 @s Codo Chudo Onuc 17 Modium•01.4 difterencesWN Windsurf Toams 214-1UTF.8ih 4 spaces...
|
NULL
|
-6383931696294804328
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormINavicareCodeKeractorlooIsFV faVsco.js?9 J PhostormINavicareCodeKeractorlooIsFV faVsco.js?9 JY-20676-delete-report-related-objectsProiectC) AutomatedReport.onp© AskAnythingPromptServiceTest.php- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv© BaseService.php> _ Midd ewareC SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.phpRequests() ListenerRoleCanr() RoleAttrTest.phpInteractionsa InvitationsListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.phpIHlohsIn Listeners© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpD McpN Modelsifinal class SavedSearchCRUDFeatureTest extends TestCase194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsOrganizationSettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}›_ serviceProvidersLocal ChangesConsole XLoo Xv Changes 16 files+ → @ Side-by-side viewer -Do not ignoreHighlight words 158 5604af40 app/Comp© ActivityController.php app/Http/Controllers/APIc)AsKAnythingPrompt.php app/Models/AsKAnythinguse Jiminny Models Role:c) ASKAnvihinaPromotbto,ono apo/comoonent/AskAnvihino/Dros© AskAnythingPromptService.php app/Component/AskAnythingC)AskAnvthinaPromotServiceTest.oho tests/Unit/Comoonent/AskAnvthinal© AskJiminnvReportsController.php app/Htto/Controllers/API/V2use liminnv Model c Team:use Jiminny Models\User;use liminnv Senvices IlsenSenvice.use Sentry:C) Constants.ono aoo/Comoonent/SClMI@ CoreUser.pho app/DTO/SCIMIAAD/Responseclass ScimProvisioningC) CoreUserRequest.ono ano/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandspublic function _construct(private readonly Dispatcher SeventDispatcherphp logging.php config@ ScimProvisionina.phnapn/Comnonent/SCIMorivace readonly userservice suserserviceC) Search.nho aon/Models/Activitvprivate readonly RoleChangeLogger SroleChangeLogger@ SoftPhoneManader.nhn ann/Comnonent/Twilio/Conference/ConferenceManad© TextMessagingService.php app/Services/Telephony• LInvorcionod Ciloc 12 filocpublic function validateUserIdInput(Sid. Team Steam): boolT'email' => Sdto->qetEmailPrimaryWorkO],SuserUodateDatalif (! SnewUser->roles()->exists()) {Snewliser->addRole User: ROLE ANALYSTO:/** @var Role Srole */Snole = Role• •quenvo->whenedr'namel => Isen: •ROI F ANAI VSTID->finctinCailo•e Hunsnell nluain hac heonolv uninctall Huncnoll without affectina the dictionarios for other lanquadec (todav 10-00)= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~pubLic tunccion conscruct userkeposicory suserkeposicorw?...29 6tpublic function shouldApplyQueries: bool{...}34 ot>public function getQueries: FilterDefinitionQueryCollection{...}A3416 ^ v45 6tpubLic function toArrayo): arrayi...h1 usaadprivate function getOptions: arrayt...=119=130public function getValue: arrayt...=131lusageprivate function getDefaultValue: arrayt...136 >public function getValidationRules(?string $prefix = null): array{...}Current versionuse Jiminny Models Role;use liminnv Model c Teamuse Jiminny Models\User;luse liminnv Senvices IlcenSenvico•use Jiminny\Contracts\Acl \RoleRepositoryInterface:use Illuminate\Support\Facades|VaLidaton.use Jiminny Rules\DependentRolesRule;use Jiminny Rules\ListenerRoleCannotHaveAdmin0rManagerPermissionRule:use Jiminny Rules \ListenerRoleRequiresListenerFeatureRuleuse Jiminny Actions UpdateUserRolesAction:use Jiminny DTO\Roles UserRolesSyncDT0:use Sentry:class Sc1mProv1s10n1nqlpublic function -_constructorivate readonly Daspatcher seventiisoatcher.private readonly UserService $userService,orivate readonly RolechangeLogger SroleChangeLogger..orivate readonly RoleRepositoryInterface $roleRepository,orivate readoniv UodatelserRolesAction SundatelserRolesAction.nublic function validatellsenTdinnut(Sid. Team Steam• hooll• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:21AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.* Reject allAccent alliAsk anvthina (4D14 @s Codo Chudo Onuc 17 Modium•01.4 difterencesWN Windsurf Toams 214-1UTF.8ih 4 spaces...
|
56400
|
NULL
|
NULL
|
NULL
|
|
56401
|
1966
|
11
|
2026-05-19T08:05:20.879903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177920879_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:20Describe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
-4716540470967220371
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:20Describe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
56399
|
NULL
|
NULL
|
NULL
|
|
56400
|
1967
|
11
|
2026-05-19T08:05:17.556155+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177917556_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-396245491663569035
|
-8634514108303561274
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
PhostormFV faVsco.js~ProjectViewavigateCode%9 JY-20676-delete-report-related-objectsKeractorTOOISWindow• suppont Dally • In 3n 0omU AskJiminnyReportActivityServiceTest~100% 5&• Tue 19 May 11:05:17ActivityAskAnythingTest.phpe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd eware> O RequestsInteractionsInvitationsJobs• ListenersD Mcp• Models• NotificationsOrganizationSettingsO PlaylistsD Policies• RepositoriesD RulesD SavedSearchesC SavedSearchCRUDFeatureTest.php> D Search›_ serviceProviders© AutomatedReport.phpC SavedSearchCRUDFeatureTest.php x]ASKAnythingPromptservice.ongC SoftPhoneManager.php() ListenerRoleCannotHaveAdminOrMaListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC) Activity/Close/Service.php© AskAnythingPromptServiceTest.php© AskAnythingPromptDto.php© BaseService.php© ScimProvisioning.phpE RoleAttrTest.php© Activity/RingCentral/Service.php© SyncActivity.php© Crm/Close/Service.php© TextMessagingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCase194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters(): voidf..}E custom.logE laravel.log4 SF [jiminny@localhost]A HS_Jocal [jiminny@localhost]A console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserin.phpxfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini vpubLic tunccion conscructuserkeposicory suserkeposicorvor...CascadeAsk Jiminny Report Mmocd → null, "status → Talsel3. loggle validation+0 ..29 0 >public function shouldApplyQueries(): boolf...}rtsController.php: 126-169 - In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt id is nult. Disabling is always allowed.4. Tests34 0 >public function getQueries: FilterDefinitionQueryCollection{...}AskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aA3 A16 A V45 € >public function toArray(): array{...}Per your rules I did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.1 usage257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch(): void{...,private function getOptions(): arrayf...}public function getValue(): arrayf...}130=131136 €>lusageprivate function getDefaultValve(): arrayf..}public function getValidationRules(?string $prefix = null): arrayf..}8 files +90 -7>Ask anything (XAL)+ <>Code Claude Opus 4.7 Medium®Reject allAccept allConsole XLooXv Changes 16 filesE.env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthinaPromptDto.php app/Component/AskAnvthina/Dtos© AskAnythingPromptService.php app/Component/AskAnything@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthina© AskJiminnyReportsController.php app/Http/Controllers/API/N2C) Constants,ono ao© CoreUser.php app/DTO/SCIM/AAD/ResponseC) CoreUserRequest.ono ano/DTO/SCIMIAAD/Request© JiminnyDebugCommand.php app/Console/Commandsphp logging.php config© ScimProvisioning.php app/Component/SCIM(C) Search.oho aon/Models/ActivitvlSoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManag© TextMessagingService.php app/Services/Telephony> Unversioned Files 13 filesDo not ignoreHighlight words"X Ba |?3 ditterences85604af40 app/Component/SCIM/Constants.phppublic const META_ENTERPRISE_SCHEMA = 'urn:ietf:params: scim: schemas: extension:enterprise:2.0:User':public const META_DEPARTMENT = 'department';public const META_META = 'meta';Current versionpublic const META META = 'meta';public const USER_PATCH_ATTR_PRIMARY_EMAIL = 'emails[type eq "work"] .value' ;public const USER_PATCH_ATTR_SECONDARY_EMAIL = 'emails[type eq "home"] . value';public const USER_PATCH_ATTR_PHONE_NUMBER = 'phoneNumbers[type eq "work"] .value' ;public const USER_PATCH_ATTR_PHONE_NUMBER_MOBILE = 'phoneNumbers[type eq "mobile"]. value';public const USER PATCH ATTR PRIMARY. EMAIL = 'emails[type eq "work"] .value' ;public const USER PATCH ATTR SECONDARY. EMAIL = 'emails[type eq "home"] . value';public const USER PATCH.ATTR_ PHONE_NUMBER = 'phoneNumbers[type eq "work"] .value' ;public const USER PATCH.ATTR_ PHONE_NUMBER MOBILE = 'phoneNumbers[type eq "mobile"] . value':public const USER PATCH ATTR_ROLES = 'roles';public const USER_SUPPORTED_PATCH_ATTRIBUTES_MUTATORS = [self::USER_PATCH_ATTR_TITLE => Mutators\Attributes\User\TitLeAttr: :class,seLt:USER PATCH ATIR ACIVE E> Mutatons Attributes User ActiveAttr::classpublic const USER SUPPORTED PATCH ATTRIBUTES MUTATORS = [selt::USERPATCH ATTRTITLE = Mutators Attributes User T1tLeAttr::classself: :USER_PATCH_ATTR_ACTIVE => Mutators\Attributes\User\ActiveAttr: :class,self::USER PATCH ATTR PRIMARY EMAIL => Mutators Attributes User Primarvemai Attr::class)self::USER_PATCH_ATTR_SECONDARY_EMAIL => Mutators\Attributes \User\SecondaryEmailAttr: :class,self::USER PATCH ATTR PHONE NUMBER => Mutators Attributes User PhoneNumberPrimarvAttr::class..self::USER_PATCH_ATTR_PHONE_NUMBER_MOBILE => Mutators\Attributes\User\PhoneNumberSecondaryAttr::class,self: :USER_PATCH_ATTR_PRIMARY_EMAIL => Mutators\Attributes\User\PrimaryEmailAttr::class,self::USER PATCH ATTR SECONDARY EMAIL => Mutators Attributes User Secondarvemai Attr::class.self::USER_PATCH_ATTR_PHONE_NUMBER => Mutators \Attributes \User\PhoneNumberPrimaryAttr::class,self::USER_PATCH_ATTR_PHONE_NUMBER_MOBILE => Mutators\Attributes\User\PhoneNumberSecondaryAttr::class,self: :USER_PATCH_ATTR_ROLES => Mutators\Attributes\User\RoleAttr::class,public const FILTER_OPERATOR_EQ = 'eq';public const FILTER, OPERATOR.EQ = 'eq';The Huncnell nluain hac họ.naries for other lanquades (todav 10-00)WN Windsurf Toams 214-1UTF.8f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56399
|
1966
|
10
|
2026-05-19T08:05:17.580168+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177917580_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-396245491663569035
|
-8634514108303561274
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:17Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A® 4887Message #the_people_of_jiminny+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56398
|
1966
|
9
|
2026-05-19T08:05:13.717063+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177913717_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1961873295031593158
|
-1483003541378331465
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages€. Vasil VasilevP. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:13Describe what you are looking for# the_people_of jimi...8 426 0• Messages@ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A® 42Message #the_people_of_jiminny+...
|
56396
|
NULL
|
NULL
|
NULL
|
|
56397
|
1967
|
10
|
2026-05-19T08:05:12.086085+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177912086_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4701279590415171657
|
-8204420343923494970
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhostormProiectINavicarecodeLaravelKeractorTOOIS• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:11FV faVsco.js?9 JY-20676-delete-report-related-objectsAskJiminnyReportActivityServiceTest vC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyx <eCascade- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd ewareRequestsAsKAnywingPromptuto.pnp© BaseService.phpC) RoleAttr.png© ScimProvisioning.php() ListenerRoleCar() RoleAttrTest.phpListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~Ask Jiminny Report Mmocd → null, "status → Talsel3. loggle validation+0 ..public function shouldApplyQueries: boolf...}rtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.a InvitationsIHlobsIn ListenersD Mcp29 6t34 0 >4. Testspublic function getQueries: FilterDefinitionOueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aA3416 ^ v45 6tpublic function toArrav@): arravf...?194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}Per your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.M Notificationsm oraanizationsettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search_ serviceProviders1usagel257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions(): array(...}!!"public function getValue: arrayt...1 usageprivate function getDefaultValue: arrayt...135136 >Ask anvthina (4D1public function getValidationRules(?string $prefix = null): arrayf...}+ <> Code Claude Opus 4.7 MediumConsoleLoo XChanges 16 tilesE.env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPrompt.php app/Models/AskAnything@ AskAnvthinaPromptDto.php app/Component/AskAnvthina/DtosC)AskAnvthinaPromotService.ono@ AskAnvthinaPromptServiceTest.pho tests/Unit/Component/AskAnvthinaDo not ignoreHighlight wordsy8 5604af40 app/Component/AskAnything/AskAnythingPromptService.phpCurrent versionnent AskAnvthina > AskAnvthinaPromotService > aet(iStardetSownerUuid,Sshareuser'sStaroet.SownerUuid,$shareUsers,SshareGroupsC) AskJiminnvRevortsController.ono aoo/Htto/Controlllers/APVN2Sprompt->has_reports,C) Constants, ono aoo/Comoonent/SClMIC) CoreUser.oho aoo/DT0/SCIMIAAD/ResoonselC) CoreUserRequest.ono aoo/DTO/SCIMIAAD/Requestrecurn [EMAIL] apn/Console/Commandsphp loadina.nho confial@ ScimProvisionina.nhn ann/Comnonent/SCIM(C) Search.oho aon/Models/Activitvl@) SoftPhoneManager.nhn ann/Comnonent/Twilio/Conference/ConferenceManadtargetSuser-›getUuido.SshareUsersUuidsSshareGroupsUulds© TextMessagingService.php app/Services/TelephonyUInvorcionod Ciloc 12 filocreturn $promptDtos:(Jiminny|Component\AskAnything > AskAnythingPromptService › create() ~StargetSuser->getUuidoSshareUsersUuids$shareGroupsUuids,Sprompt->has reportsSnewPromot->aetTargetolSsharelsersUuids.SnewPromot->getTaraetolSuser->getUuidO.SsharelsersUuidsSnewPrompt->has reports,* Reject allAccent alli•01.4 difterencesnublic function deletedHunsnoll without affectina the dictionarios for other lanquades (todav 10-00)WN Windsurf Toams 214-1UTF.8io 4 spaces...
|
56395
|
NULL
|
NULL
|
NULL
|
|
56396
|
1966
|
8
|
2026-05-19T08:05:12.112553+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177912112_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-396245491663569035
|
-8634514108303561274
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messages€. Vasil VasilevP.. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:12Describe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56395
|
1967
|
9
|
2026-05-19T08:05:11.174848+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177911174_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1356238323261734672
|
-8996598804339809755
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56394
|
1967
|
8
|
2026-05-19T08:05:10.109116+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177910109_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6966022010367698874
|
-564708849257816637
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
PhostormProiectVIewINavicareCodeKeractorTOOISFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd ewareRequestsl Interactions© BaseService.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~C SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php() ListenerRoleCanr() RoleAttrTest.php29 6tpublic function shouldApplyQueries: bool{...}a InvitationsIHlobsIn ListenersD McpN ModelsiM NotificationsListenerRoleCannotHaveAdminOrManagerPermissionRule.php© Activity/Close/Service.php© Activity/RingCentral/Service.php34 6tpublic function getQueries: FilterDefinitionQueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3 416 A V45 6tpublic function toArrav@): arravs..?194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}1usagelD OrganizationSettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search_ serviceProviders257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions(): array(...}118public function getValue: arrayt...- 1311 usage=131135136 €t>private function getDefaultValue(): arrayf...}public function getValidationRules(?string $prefix = null): arrayf...}ConsoleLoo Xv Changes 16 filesE.env.local app© ActivityController.php app/Http/Controllers/API@ AskAnythingPrompt.php app/Models/AskAnythingDo not ignoreHighlight words"8 5604af40 app/Component/AskAnyt|Current versionpublic AskAnythingPromptTarget $target,nuhlie Dstrina CownenTd = null.C) ASKAnvihinaPromotDto.ono apo/e© AskAnvthinaPromptService.php app/Component/AskAnvthing@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthinapublic ?array $shareUsers = nullpublic ?array $shareGroups = null,C)AskJiminnvRenortsController.oho apo/Htto/Controllers/APIV2public AskAnythingPromptTarget $target,public ?string $ownerId = null,public ?array $shareUsers = null,public ?array $shareGroups = null,nublic hool ShacRenonts = falcex <eC) Constants, ono aoo/Comoonent/SClMI@ CoreUser.php app/DTO/SCIMIAAD/ResponseC) CoreUserRequest.ono ano/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandsphp loadina.nho confial@ ScimProvisionina.nhn ann/Comnonent/SCIM'id' => Sthis->idulcle => schis-oclcle(C) Search.oho aon/Models/Activitvl'content' => Sthis-›content@) SoftPhoneManager.ohn ann/Comnonent/Twilio/Conference/ConferenceManad'target' => sthis->target->namel'id' = Sthis->id'title' => Sthis-›title'content' => Sthis-›content'target' => Sthis-›target-›name© TextMessagingService.php app/Services/TelephonyUInvorcionod Ciloc 12 filocif (Sthis->ownerId !== null) {Scatalownen 1d' = sthls->ownerild:if ($this->ownerId |== null) {Sdatal 'ownen id'] = Sthis->ownerld:loc for other lanquades (todav 10-00)• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:09AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0..mocd → null, "status → Talsel3. loggle validationrtsController.php: 126-169 - In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.Ask anvthina (4D1+ <> Code Claude Opus 4.7 Medium* Reject allAccent alli•01.2 differencesWN Windsurf Toams 214-1UTF.8|Po 4 spaces...
|
56393
|
NULL
|
NULL
|
NULL
|
|
56393
|
1967
|
7
|
2026-05-19T08:05:07.829118+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177907829_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormProiectINavicareCodeKeractorTOOISFV faVsco PhostormProiectINavicareCodeKeractorTOOISFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd ewareRequestsl Interactionsa InvitationsIHlobsIn ListenersD Mcp© BaseService.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~C SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php() ListenerRoleCannotHaveAdminOrMa() RoleAttrTest.php29 6tpublic function shouldApplyQueries: bool{...}ListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.php34 6t >public function getQueries: FilterDefinitionQueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3416 ^ v45 6toublic function toArravo: arrav..."194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsD OrganizationSettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search_ serviceProviders1 usaad257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions(): array(...}118public function getValue: arrayt...- 1311 usage8 files 400-7 )=131135136 €t>private function getDefaultValue: arrayt...Ask anvthina (4D1public function getValidationRules(?string $prefix = null): arrayf...}+ <> Code Claude Opus 4.7 MediumConsole XLoo Xv Changes 16 files=.env.local appActivityController.php app/Http/Controllers/APl© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthinaPromptDto.php app/Component/AskAnvthina/Dtos© AskAnvthinaPromptService.php app/Component/AskAnvthina@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthinaSide-by-side viewerDo not ignore5604af40 app/Http/Controllers/API/ActivityController.phpX Ba |?Current versionpublic function deleteActivitySearch(Request Srequest, Search $search): JsonResponsepublic function deleteActivitySearch(Request Srequest, Search $search): JsonResponseSthis-sauthonize("deletel Scpanch)-Sthis-sauthonize("deletel Sceanch)C) AskJiminnvRevortsController.ono aoo/Htto/Controlllers/APVN2// Orpan any AutomatedReports that use this searciC) Constants.ono apo/Comoonent/SClMISsearch->automatedReports->withTrashed->update(['activity_search_id' => null]):@ CoreUser.php app/DTO/SCIMIAAD/ResponseC) CoreUserRequest.ono aoo/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandsphp loadina.nho confial@ ScimProvisionina.nhn ann/Comnonent/SCIM// Delete filters and the search itself// Disable andany AutomatedReports that use this searchSsearch->automatedReports->withTrashed->update(['activity_search_id' => null,letatuci = folcossearch-rilters->delete/I Delete filters and the search itself(C) Search.oho aon/Models/ActivitvlSsearch->filters->deleteO:@) SoftPhoneManager.ohn ann/Comnonent/Twilio/Conference/ConferenceManadSsearch->deleteo© TextMessagingService.php app/Services/TelephonyUInvorcionod Ciloc 12 filoc• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:07U AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..mocd → null, "status → Talsel3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.* Reject allAccent all•01.1ditterenceloc for other lanquades (todav 10-00)WN Windsurf Toams 214-1UTF.8Po 4 spaces...
|
NULL
|
-6279436345858547285
|
NULL
|
click
|
ocr
|
NULL
|
PhostormProiectINavicareCodeKeractorTOOISFV faVsco PhostormProiectINavicareCodeKeractorTOOISFV faVsco.js?9 JY-20676-delete-report-related-objectsC) AutomatedReport.onp© AskAnythingPromptServiceTest.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euy- ActivtvaskAnvihind est.ono© SavedSearchCRUDFeatureTest.php X© AskAnythingPromptDto.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xe) -xooricontro||erRateLimit est.onoC) FakeProphetClient.ohv> _ Midd ewareRequestsl Interactionsa InvitationsIHlobsIn ListenersD Mcp© BaseService.phpfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefini~C SoftPhoneManager.phpC) RoleAttr.png© ScimProvisioning.php() ListenerRoleCannotHaveAdminOrMa() RoleAttrTest.php29 6tpublic function shouldApplyQueries: bool{...}ListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC)Activity/Close/Service.php© Activity/RingCentral/Service.php34 6t >public function getQueries: FilterDefinitionQueryCollection{...}© SyncActivity.php© Crm/Close/Service.php(C) TextMessaqingService.phpfinal class SavedSearchCRUDFeatureTest extends TestCaseA3416 ^ v45 6toublic function toArravo: arrav..."194 D >public function userCanUpdateASavedSearchBasedOnActivityFilters: void{...}M NotificationsD OrganizationSettingsW rlayllstsD Policies( RepositoriesC RulesC SavedSearchesSavedSearchCRUDFeatureTest.php› C Search_ serviceProviders1 usaad257 D >#Phrun1c rramework Accribuces lescpublic function userCanDeleteASavedSearch: void{...}private function getOptions(): array(...}118public function getValue: arrayt...- 1311 usage8 files 400-7 )=131135136 €t>private function getDefaultValue: arrayt...Ask anvthina (4D1public function getValidationRules(?string $prefix = null): arrayf...}+ <> Code Claude Opus 4.7 MediumConsole XLoo Xv Changes 16 files=.env.local appActivityController.php app/Http/Controllers/APl© AskAnythingPrompt.php app/Models/AskAnything© AskAnvthinaPromptDto.php app/Component/AskAnvthina/Dtos© AskAnvthinaPromptService.php app/Component/AskAnvthina@ AskAnvthinaPromptServiceTest.php tests/Unit/Component/AskAnvthinaSide-by-side viewerDo not ignore5604af40 app/Http/Controllers/API/ActivityController.phpX Ba |?Current versionpublic function deleteActivitySearch(Request Srequest, Search $search): JsonResponsepublic function deleteActivitySearch(Request Srequest, Search $search): JsonResponseSthis-sauthonize("deletel Scpanch)-Sthis-sauthonize("deletel Sceanch)C) AskJiminnvRevortsController.ono aoo/Htto/Controlllers/APVN2// Orpan any AutomatedReports that use this searciC) Constants.ono apo/Comoonent/SClMISsearch->automatedReports->withTrashed->update(['activity_search_id' => null]):@ CoreUser.php app/DTO/SCIMIAAD/ResponseC) CoreUserRequest.ono aoo/DTO/SCIMIAAD/[EMAIL] apn/Console/Commandsphp loadina.nho confial@ ScimProvisionina.nhn ann/Comnonent/SCIM// Delete filters and the search itself// Disable andany AutomatedReports that use this searchSsearch->automatedReports->withTrashed->update(['activity_search_id' => null,letatuci = folcossearch-rilters->delete/I Delete filters and the search itself(C) Search.oho aon/Models/ActivitvlSsearch->filters->deleteO:@) SoftPhoneManager.ohn ann/Comnonent/Twilio/Conference/ConferenceManadSsearch->deleteo© TextMessagingService.php app/Services/TelephonyUInvorcionod Ciloc 12 filoc• suppont Dally • In 3n 0om100% 5• Tue 19 May 11:05:07U AskJiminnyReportActivityServiceTest vCascadeAsk Jiminny Report M+0 ..mocd → null, "status → Talsel3. loggle validationrtsController.php: 126-169 — In toggleStatus(), when enabled=true and the report is an Ask Jiminnyreport, returns 422 lt elther activity search 1d or ask anything prompt 1d is nult . Disabling is always allowed.4. TestsAskAnythingPromptServiceTest.php - Updated two existing update() mock expectations to match the new payload ( ['aPer your rules i did not add unit tests for the controller. The existing SavedSearchCRUDFeatureTest covers the searchdelete tlow: backend service test covers promot delete pavload.* Reject allAccent all•01.1ditterenceloc for other lanquades (todav 10-00)WN Windsurf Toams 214-1UTF.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56392
|
1966
|
7
|
2026-05-19T08:05:09.381279+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177909381_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.02013889,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6509625738266471330
|
-8996616125002602716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
56391
|
NULL
|
NULL
|
NULL
|
|
56391
|
1966
|
6
|
2026-05-19T08:05:07.853282+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177907853_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messages€. Vasil VasilevP.. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:07Describe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
-3195253849953379698
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (-zsh)₴82app/Jobs/Crm/MatchActivityCrmData.phpapp/Jobs/Middleware/HandleHubspotRateLimit.phpAPP (-zst47++++++p42++++++*app/Mcp/Servers/JiminnyServer.phpapp/Mcp/Tools/GetMeTool.php157++++++•app/Models/Feature/FeatureEnum.phpapp/Services/Activity/HubSpot/ProviderResolver.php+-app/Services/Activity/HubSpot/ProviderResolverInterface.phpapp/Services/Activity/HubSpot/Providers/Provider.phpapp/Services/Activity/HubSpot/Providers/ProviderKixie.php+-app/Services/Activity/HubSpot/Providers/Provider0rum.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilio.phpapp/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.phpapp/Services/Activity/HubSpot/Service.phpapp/Services/Crm/Hubspot/Client.phpapp/Services/Crm/Hubspot/HubspotClientInterface.phpapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.phpapp/Services/Crm/Hubspot/Pagination/PaginationState.phpdatabase/migrations/2026_05_13_124153_create_mcp_feature_flag.phptests/Feature/Mcp/GetMeToolFeatureTest.phptests/Feature/Mcp/ListCallsToolFeatureTest.phptests/Feature/Mcp/McpTestHelpersTrait.phptests/Unit/Component/ES/ChunkSizeTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.phptests/Unit/Exceptions/RateLimitExceptionTest.phptests/Unit/Jobs/Crm/MatchActivityCrmDataTest.phptests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phptests/Unit/Services/Activity/HubSpot/ServiceTest.phptests/Unit/Services/Crm/Hubspot/ClientTest.phptests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php97++++++-132++++++•15++++++21++++--2+-25++++++.140++++++-2+-19++++++50++++++-27++++++139++++++-6856++++++-+-151++++++-109++++++-250++++++-283++++++=37 files changed, 1587 insertions(+), 344 deletions(-)create mode 100644 app/Component/ES/ChunkSize.phpcreate mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.phpcreate mode 100644 app/Mcp/Tools/GetMeTool.phpcreate mode 100644 database/migrations/2026_05_13_124153_create_mcp_feature_flag.phpcreate mode 100644 tests/Feature/Mcp/GetMeToolFeatureTest.phpcreate mode 100644 tests/Unit/Component/ES/ChunkSizeTest.phpcreate mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.phpcreate mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullAlready up to date.lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20676-delete-reSwitched to a new branch 'JY-20676-delete-report-related-objects'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEDHomeDMsActivityFilesLaterMoreJiminny ...# Jiminny-Dg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messages€. Vasil VasilevP.. Nikolay Yankov%. Galya Dimitrova L. Aneliya Angelovaão Stefka StoyanovaStoyan Tomovd Todor Stamatov "Kd. Mario GeorgievNikolay Ivanovdo James Graham2 Stoyan TanevRo Steliyan GeorgievLukas Kovalik y…..AppsJira CloudToastSupport Daily - in 3 h 55 m100% <7Tue 19 May 11:05:07Describe what you are looking for# the_people_of jimi...8 426 0• MessagesQ Files+Stefka Stoyanov:Today ~Hі Eteam,I wanted to share that our Lead Developer llianKyuchukov has decided to move on from Jiminny topursue a new opportunity. While we're really sad tosee them go, we're also excited for him and thisnext chapter in his professional journey.llian's last working day will be 14 July, so we stillhave some time together before then. Over thecoming weeks, please make the most of hisknowledge, experience, and context so we canensure a smooth transition across the team.It's hard to overstate the impact llian has had here- from shaping the product and solving toughproblems to supporting teammates and helpingbuild the product we have today. We re incrediblygrateful for everything he contributed over this 2years he was with & engineering team.Just wanted to share the news openly witheveryone and say a huge thanks to llian foreverything so far. Vou Please join me in wishing himall the very best for what comes next.We start looking for a Full-Stack Software Engineer,so if you know anyone you believe will fit, pleasecontact me or Mira directly A687Message #the_people_of_jiminny+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56390
|
1967
|
6
|
2026-05-19T08:05:06.989757+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177906989_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1356238323261734672
|
-8996598804339809755
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}...
|
56387
|
NULL
|
NULL
|
NULL
|
|
56389
|
1966
|
5
|
2026-05-19T08:05:06.977760+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177906977_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3827195245788680489
|
-8996616126076344540
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…...
|
56388
|
NULL
|
NULL
|
NULL
|
|
56388
|
1966
|
4
|
2026-05-19T08:04:56.847904+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177896847_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6509625738266471330
|
-8996616125002602716
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56387
|
1967
|
5
|
2026-05-19T08:04:37.800088+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177877800_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.390625,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"16","depth":4,"bounds":{"left":0.4005984,"top":0.22426178,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.22266561,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":0.49162012},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
6509625738266471330
|
-8996616125002602716
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
16
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
56386
|
1967
|
4
|
2026-05-19T08:04:28.681879+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177868681_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function deletingSavedSearchDisablesRelatedReports(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
$report = \Jiminny\Models\AutomatedReport::factory()->create([
'team_id' => $team->id,
'type' => 'ask_jiminny',
'status' => true,
'activity_search_id' => $search->id,
'created_by' => $user->id,
]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200);
$this->assertDatabaseHas('automated_reports', [
'id' => $report->id,
'activity_search_id' => null,
'status' => false,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function deletingSavedSearchDisablesRelatedReports(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n $report = \\Jiminny\\Models\\AutomatedReport::factory()->create([\n 'team_id' => $team->id,\n 'type' => 'ask_jiminny',\n 'status' => true,\n 'activity_search_id' => $search->id,\n 'created_by' => $user->id,\n ]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200);\n\n $this->assertDatabaseHas('automated_reports', [\n 'id' => $report->id,\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.26728722,"height":0.5442937},"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function deletingSavedSearchDisablesRelatedReports(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n $report = \\Jiminny\\Models\\AutomatedReport::factory()->create([\n 'team_id' => $team->id,\n 'type' => 'ask_jiminny',\n 'status' => true,\n 'activity_search_id' => $search->id,\n 'created_by' => $user->id,\n ]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200);\n\n $this->assertDatabaseHas('automated_reports', [\n 'id' => $report->id,\n 'activity_search_id' => null,\n 'status' => false,\n ]);\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":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"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}]...
|
-6920777432722650556
|
-9140731309851991260
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function deletingSavedSearchDisablesRelatedReports(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
$report = \Jiminny\Models\AutomatedReport::factory()->create([
'team_id' => $team->id,
'type' => 'ask_jiminny',
'status' => true,
'activity_search_id' => $search->id,
'created_by' => $user->id,
]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200);
$this->assertDatabaseHas('automated_reports', [
'id' => $report->id,
'activity_search_id' => null,
'status' => false,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
56385
|
NULL
|
NULL
|
NULL
|
|
56354
|
1964
|
4
|
2026-05-19T08:00:10.152340+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177610152_m1.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function deletingSavedSearchDisablesRelatedReports(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
$report = \Jiminny\Models\AutomatedReport::factory()->create([
'team_id' => $team->id,
'type' => 'ask_jiminny',
'status' => true,
'activity_search_id' => $search->id,
'created_by' => $user->id,
]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200);
$this->assertDatabaseHas('automated_reports', [
'id' => $report->id,
'activity_search_id' => null,
'status' => false,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php, class
ActivityChannel.php, final class
ActivityDurationRange.php, class
ActivityFilter.php, final class
ActivityPlaylistIn.php, final class
ActivityProviderIn.php, final class
ActivityRecorded.php, class
ActivityRecordingStopped.php, final class
ActivityScheduledDate.php, final class
ActivityStatusIn.php, class
ActivityType.php, final class
ActivityUpdatedDate.php, final class
AiCallScoreFilter.php, final class
AutoScoreFilter.php, final class
ClosedDealsFilter.php, final class
CoachingFeedbackAverageScore.php, final class
CoachingFeedbackCoachUserIn.php, final class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function deletingSavedSearchDisablesRelatedReports(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n $report = \\Jiminny\\Models\\AutomatedReport::factory()->create([\n 'team_id' => $team->id,\n 'type' => 'ask_jiminny',\n 'status' => true,\n 'activity_search_id' => $search->id,\n 'created_by' => $user->id,\n ]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200);\n\n $this->assertDatabaseHas('automated_reports', [\n 'id' => $report->id,\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Tests\\Feature\\SavedSearches;\n\nuse Illuminate\\Support\\LazyCollection;\nuse Illuminate\\Support\\Str;\nuse Mockery\\MockInterface;\nuse Illuminate\\Database\\Eloquent\\Factories\\Sequence;\nuse Illuminate\\Foundation\\Testing\\DatabaseTransactions;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\nuse Jiminny\\Events\\Nudges\\NudgeCreated;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Role;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Tests\\TestCase;\n\nfinal class SavedSearchCRUDFeatureTest extends TestCase\n{\n use DatabaseTransactions;\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanGetAListOfSavedSearches(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'status' => User::STATUS_ACTIVE,\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create();\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n $nudges = Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->getJson(route('api.saved_search.index'))\n ->assertStatus(200)\n ->assertJson(\n fn (AssertableJson $json) =>\n $json->has(1)\n ->first(\n fn ($json) =>\n $json->where('id', $search->id_string)\n ->where('name', $search->getName())\n ->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])\n ->has('nudges', 2)\n ->has(\n 'nudges.0',\n fn ($json) =>\n $json->where('id', $nudges[0]->id_string)\n ->where('frequency', 'daily')\n ->where('channel', 'email')\n ->has('expiresAt')\n )\n ->has(\n 'nudges.1',\n fn ($json) =>\n $json->where('id', $nudges[1]->id_string)\n ->where('frequency', 'weekly')\n ->where('channel', 'slack')\n ->has('expiresAt')\n )\n )\n );\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanCreateASavedSearchBasedOnActivityFilters(): void\n {\n $this->markTestSkipped('Needs to be fixed');\n\n Event::fake();\n\n $this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {\n $mock->shouldReceive('onDemandScroll')\n ->times(2)\n ->andReturn(LazyCollection::empty());\n });\n\n /** @var Team $team */\n $team = Team::factory()->create([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1, // temporary\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->createQuietly([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->postJson(route('api.saved_search.create'), [\n 'name' => '::searchName::',\n 'filters' => [\n 'min_duration' => 1,\n 'only_recorded' => 1,\n ],\n 'nudges' => [\n [\n 'frequency' => 'daily',\n 'channel' => 'email',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n [\n 'frequency' => 'daily',\n 'channel' => 'slack',\n 'expiresAt' => now()->addWeek()->format('Y-m-d'),\n ],\n ],\n ])\n ->assertStatus(200);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => '::searchName::',\n 'user_id' => $user->id,\n ]);\n\n $search = $user->searches()->first();\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 2,\n ]);\n\n $this->assertDatabaseHas('nudges', [\n 'activity_search_id' => $search->id,\n 'frequency' => 2,\n 'channel' => 4,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'min_duration',\n 'value' => 1,\n ]);\n\n $this->assertDatabaseHas('activity_search_filters', [\n 'activity_search_id' => $search->id,\n 'filter' => 'only_recorded',\n 'value' => 1,\n ]);\n\n Event::assertDispatched(NudgeCreated::class, 2);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanUpdateASavedSearchBasedOnActivityFilters(): void\n {\n /** @var Team $team */\n $team = Team::factory()->createQuietly([\n 'uuid' => Str::uuid()->toString(),\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->putJson(route('api.saved_search.update', $search->id_string), [\n 'name' => 'Updated Search Name',\n ])->assertOk()\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'name' => 'Search Name',\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseHas('activity_searches', [\n 'name' => 'Updated Search Name',\n 'user_id' => $user->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function userCanDeleteASavedSearch(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n SearchFilter::factory()\n ->for($search)\n ->count(2)\n ->state(new Sequence(\n ['filter' => 'min_engaging_questions', 'value' => 3],\n ['filter' => 'max_customer_questions', 'value' => 5],\n ))->create();\n\n Nudge::factory()\n ->count(2)\n ->state(\n new Sequence(\n ['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],\n ['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],\n )\n )\n ->create(['activity_search_id' => $search->id]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200)\n ->assertJson([]);\n\n $this->assertDatabaseMissing('activity_searches', [\n 'user_id' => $user->id,\n ]);\n\n $this->assertDatabaseMissing('activity_search_filters', [\n 'activity_search_id' => $search->id,\n ]);\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\Test]\n public function deletingSavedSearchDisablesRelatedReports(): void\n {\n /** @var Team $team */\n $team = Team::factory()->create([\n 'crm_id' => 1,\n ]);\n\n $adminRole = Role::firstWhere('name', User::ROLE_ADMIN);\n\n /** @var User $user */\n $user = User::factory()\n ->hasAttached($adminRole, ['user_type' => 'user'])\n ->create([\n 'name' => '::userName::',\n 'team_id' => $team->id,\n ]);\n\n $team->owner_id = $user->id;\n $team->save();\n\n $search = Search::factory()->for($user)->create([\n 'name' => 'Search Name',\n ]);\n\n $report = \\Jiminny\\Models\\AutomatedReport::factory()->create([\n 'team_id' => $team->id,\n 'type' => 'ask_jiminny',\n 'status' => true,\n 'activity_search_id' => $search->id,\n 'created_by' => $user->id,\n ]);\n\n $this->apiGuardLogin($user);\n\n $this->actingAs($user)\n ->deleteJson(route('api.saved_search.delete', $search->id_string))\n ->assertStatus(200);\n\n $this->assertDatabaseHas('automated_reports', [\n 'id' => $report->id,\n 'activity_search_id' => null,\n 'status' => false,\n ]);\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":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityChannel.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityDurationRange.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityPlaylistIn.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityProviderIn.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecorded.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityRecordingStopped.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityScheduledDate.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityStatusIn.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityType.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityUpdatedDate.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoreFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutoScoreFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ClosedDealsFilter.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackAverageScore.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbackCoachUserIn.php, final class","depth":11,"on_screen":false,"role_description":"text"}]...
|
5437592286218694654
|
-8996616400949007580
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Tests\Feature\SavedSearches;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Mockery\MockInterface;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Testing\Fluent\AssertableJson;
use Jiminny\Events\Nudges\NudgeCreated;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Nudge;
use Jiminny\Models\Role;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Tests\TestCase;
final class SavedSearchCRUDFeatureTest extends TestCase
{
use DatabaseTransactions;
#[\PHPUnit\Framework\Attributes\Test]
public function userCanGetAListOfSavedSearches(): void
{
$this->markTestSkipped('Needs to be fixed');
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'status' => User::STATUS_ACTIVE,
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create();
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
$nudges = Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->getJson(route('api.saved_search.index'))
->assertStatus(200)
->assertJson(
fn (AssertableJson $json) =>
$json->has(1)
->first(
fn ($json) =>
$json->where('id', $search->id_string)
->where('name', $search->getName())
->where('filters', ['max_customer_questions' => 5, 'min_engaging_questions' => 3])
->has('nudges', 2)
->has(
'nudges.0',
fn ($json) =>
$json->where('id', $nudges[0]->id_string)
->where('frequency', 'daily')
->where('channel', 'email')
->has('expiresAt')
)
->has(
'nudges.1',
fn ($json) =>
$json->where('id', $nudges[1]->id_string)
->where('frequency', 'weekly')
->where('channel', 'slack')
->has('expiresAt')
)
)
);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanCreateASavedSearchBasedOnActivityFilters(): void
{
$this->markTestSkipped('Needs to be fixed');
Event::fake();
$this->partialMock(ElasticActivityRepository::class, function (MockInterface $mock) {
$mock->shouldReceive('onDemandScroll')
->times(2)
->andReturn(LazyCollection::empty());
});
/** @var Team $team */
$team = Team::factory()->create([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1, // temporary
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->createQuietly([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$this->apiGuardLogin($user);
$this->actingAs($user)
->postJson(route('api.saved_search.create'), [
'name' => '::searchName::',
'filters' => [
'min_duration' => 1,
'only_recorded' => 1,
],
'nudges' => [
[
'frequency' => 'daily',
'channel' => 'email',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
[
'frequency' => 'daily',
'channel' => 'slack',
'expiresAt' => now()->addWeek()->format('Y-m-d'),
],
],
])
->assertStatus(200);
$this->assertDatabaseHas('activity_searches', [
'name' => '::searchName::',
'user_id' => $user->id,
]);
$search = $user->searches()->first();
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 2,
]);
$this->assertDatabaseHas('nudges', [
'activity_search_id' => $search->id,
'frequency' => 2,
'channel' => 4,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'min_duration',
'value' => 1,
]);
$this->assertDatabaseHas('activity_search_filters', [
'activity_search_id' => $search->id,
'filter' => 'only_recorded',
'value' => 1,
]);
Event::assertDispatched(NudgeCreated::class, 2);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanUpdateASavedSearchBasedOnActivityFilters(): void
{
/** @var Team $team */
$team = Team::factory()->createQuietly([
'uuid' => Str::uuid()->toString(),
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->putJson(route('api.saved_search.update', $search->id_string), [
'name' => 'Updated Search Name',
])->assertOk()
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'name' => 'Search Name',
'user_id' => $user->id,
]);
$this->assertDatabaseHas('activity_searches', [
'name' => 'Updated Search Name',
'user_id' => $user->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function userCanDeleteASavedSearch(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
SearchFilter::factory()
->for($search)
->count(2)
->state(new Sequence(
['filter' => 'min_engaging_questions', 'value' => 3],
['filter' => 'max_customer_questions', 'value' => 5],
))->create();
Nudge::factory()
->count(2)
->state(
new Sequence(
['frequency' => 2, 'channel' => 2, 'expires_at' => now()->addWeek()],
['frequency' => 4, 'channel' => 4, 'expires_at' => now()->addMonth()],
)
)
->create(['activity_search_id' => $search->id]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200)
->assertJson([]);
$this->assertDatabaseMissing('activity_searches', [
'user_id' => $user->id,
]);
$this->assertDatabaseMissing('activity_search_filters', [
'activity_search_id' => $search->id,
]);
}
#[\PHPUnit\Framework\Attributes\Test]
public function deletingSavedSearchDisablesRelatedReports(): void
{
/** @var Team $team */
$team = Team::factory()->create([
'crm_id' => 1,
]);
$adminRole = Role::firstWhere('name', User::ROLE_ADMIN);
/** @var User $user */
$user = User::factory()
->hasAttached($adminRole, ['user_type' => 'user'])
->create([
'name' => '::userName::',
'team_id' => $team->id,
]);
$team->owner_id = $user->id;
$team->save();
$search = Search::factory()->for($user)->create([
'name' => 'Search Name',
]);
$report = \Jiminny\Models\AutomatedReport::factory()->create([
'team_id' => $team->id,
'type' => 'ask_jiminny',
'status' => true,
'activity_search_id' => $search->id,
'created_by' => $user->id,
]);
$this->apiGuardLogin($user);
$this->actingAs($user)
->deleteJson(route('api.saved_search.delete', $search->id_string))
->assertStatus(200);
$this->assertDatabaseHas('automated_reports', [
'id' => $report->id,
'activity_search_id' => null,
'status' => false,
]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\ActivitySearch\FilterDefinition;
use Illuminate\Support\Collection;
use Jiminny\Component\ActivitySearch\FilterDefinition;
use Jiminny\Component\ActivitySearch\FilterDefinitionQuery;
use Jiminny\Component\ActivitySearch\FilterDefinitionQueryCollection;
use Jiminny\Contracts\ActivitySearch\ValidatedFilterDefinitionInterface;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Models;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Traits\RequiresUUID;
use Elastica\Query;
final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface
{
private const int NO_GROUP_ID = 999;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function shouldApplyQueries(): bool
{
return count($this->getValue()) > 0;
}
public function getQueries(): FilterDefinitionQueryCollection
{
$userIds = $this->getValue();
return FilterDefinitionQueryCollection::make([
FilterDefinitionQuery::instance()
->setQuery(new Query\Terms('coachingFeedbacks.coach.id_string', $userIds))
->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),
]);
}
public function toArray(): array
{
return [
'id' => 'coaching-feedback-user-filter',
'label' => 'Coach',
'helpText' => 'The person who completed the Coaching Framework',
'placeholder' => 'Search coaches',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'queryParam' => 'coaching_feedback_coach_id',
'options' => $this->getOptions(),
'groupLabelKey' => 'label',
'groupValuesKey' => 'users',
'optionLabelKey' => 'name',
'optionValueKey' => 'id',
'value' => $this->getValue(),
];
}
private function getOptions(): array
{
$user = $this->executedBy;
$query = $user
->getTeam()
->users()
->whereNotIn(
'uuid',
$this->userRepository->findInaccessibleDeactivatedUserUuids($user)
->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))
->all(),
)
->orderBy('name');
$users = $query
->with('group')
->get()
->reduce(
static function (array $carry, Models\User $user): array {
$group = $user->getGroup();
$groupId = $group !== null
? $group->getUuid()
: self::NO_GROUP_ID;
$groupName = $group !== null
? $group->getName()
: '(NON-GROUPED)';
if (! array_key_exists($groupId, $carry)) {
$carry[$groupId] = [
'label' => $groupName,
'users' => [],
];
}
$carry[$groupId]['users'][] = [
'id' => $user->getUuid(),
'name' => sprintf(
'%s%s',
$user->getName(),
! $user->isStatusActive()
? ' (Inactive)'
: ''
),
];
return $carry;
},
[]
);
return Collection::make($users)
->sortBy('label')
->values()
->toArray();
}
public function getValue(): array
{
if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {
return $this->getDefaultValue();
}
return array_diff(
$this->criteria->getCoachingFeedbackCoachUserId(),
$this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),
);
}
private function getDefaultValue(): array
{
return [];
}
public function getValidationRules(?string $prefix = null): array
{
$team = $this->executedBy->getTeam();
return [
$prefix . 'coaching_feedback_coach_id' => 'array',
$prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),
];
}
public function getSortOrder(): int
{
return 86;
}
public function shouldBeIncluded(Team $team): bool
{
return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
DealInsights, folder
Security, folder
TeamInsights, folder
ActivityActualDate.php, class
ActivityChannel.php, final class
ActivityDurationRange.php, class
ActivityFilter.php, final class
ActivityPlaylistIn.php, final class
ActivityProviderIn.php, final class
ActivityRecorded.php, class
ActivityRecordingStopped.php, final class
ActivityScheduledDate.php, final class
ActivityStatusIn.php, class
ActivityType.php, final class
ActivityUpdatedDate.php, final class
AiCallScoreFilter.php, final class
AutoScoreFilter.php, final class
ClosedDealsFilter.php, final class
CoachingFeedbackAverageScore.php, final class
CoachingFeedbackCoachUserIn.php, final class...
|
56350
|
NULL
|
NULL
|
NULL
|
|
56353
|
1965
|
10
|
2026-05-19T07:59:58.361573+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177598361_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiectC) AutomatedReport.onp© AskAnythingPromptServiceTest.php> DVOphp helpers.phpC) InitialFrontendState.pnp© Jiminny.phpC Plan.pnp© Serializer.php© TeamScimDetails.php> @ bootstrap> O build>@ config|>CJ contrib|> @ database>O docsu troni-end>C) langnode modules library root> O phpstan> public>O resourcesv h routesphp ani.oho3hp api_v2.phpphp console.ohophp customer_api.phpphp embedded.phpphe health nhnpho seim nhnlphp uprotected_web.phpohp web.onephp webhook.php>@ scripts>@J storagevMtocte› O Feature> D Integration> D Services> D Stubsv D Unit> O Actionscomponent•DActionltems→DActivitv> M ActivitvAnalvticsM ActivitvSearch• MAiActi vitvivoeM AiAutomation>M AiCallScorind• M AckAnvthinglc SavedSearchCRUDFeaturelest.onp x©AskAnythingPromptDto.php© BaseService.phpC) RoleAttr.png© ScimProvisioning.phpC) ListenerRoleCa() RoleAttrTest.phpListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC) Activity/Close/Service.php© Activity/RingCentral/Service.phpC) SyncActivity.php(C) Crm/Close/Service.phoInamesnace Tests Feature SavedSearches .Use ...22 Dfinal class SavedSearchCRUDFeatureTest extends TestCaseuse varabaselransactions27 D >#[\PHPUnit\Framework\Attributes\Test]public function userCanGetAList0fSavedSearches: void{...}104 D >#[\PHPUnit\Framework\Attributes\Testlpublic function userCanCreateASavedSearchBasedOnActivityFilters: void{...}194 07# PhPun1t Framework Attributes lestpublic function userCanUpdateASavedSearchBased0nActivityFilters@: void{...}257 0 >#|PHPUnit Framework Attributes Testpublic function userCanDeleteASavedSearchO: void{...}lAccent Reiect#PHPUnit Framework Attributes Test316 D>public function deletingSavedSearchDisablesRelatedReportsO: voids....c AckAnvthinaPromntServicetlect nhnC HistorvServiceTest nhn> M Ack liminnvAAccept File &+X Reject File 4%€+8 of 8 files →ries for other languages. (59 minutes ago)= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php X<?phpdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tysearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 ct ›146 @t>150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}"suppont Dally • In 4h 1m100% 5• Tue 19 May 10:59:58AskJiminnyReportActivityServiceTest vCascadeASK Jiminny Report+0 ..AskJiminnyReportsController.php+12 -1Thoughtsw6/7 tasks done• Add validation to toggleStatus to prevent enabling reports with null references• Write unit tests for new functionalitySearched SearchTest in testsSearched ASKAnythingPromptlest in testsSoarched Ack.liminnvRenorteControllerTect in tocteSearched Activity. Search in testsSearched ActivityControllerTest in testsThought for 1s›Read SavedSearchCRUDFeatureTest.ohdSearched AskJiminnyReportsController in testsThought for 1s>Ihought for 1s>Error while editina AskAnvthinaPromotServiceTest.ohoThouaht for 1s >error while editing ASKAnychingPromptservicelest.pnp ›Thought for 1s>• AskAnythingPromptServiceTest.php+1-1Inoughts• AskAnythingPromptServiceTest.php+1-1• SavedSearchCRUDFeatureTest.phpThe user has asked me to imolement loqic for handlina Ask Jiminny reports when related saved searches or ask-liminnvprompts are deleted. Let me review what l've done so far.1. Added has_reports attribute to Search model2. V Added has reports attribute to AskAnvthinaPromot model and DTO.3. VUodated saved search deletion to disable related renorts .4. 7 Updated ask-liminny prompt deletion to disable related reports* Reiect allAccent allAsk anvthing 884-D)« Code SWF-1.6WN Windsurf Teams214-1UTE.8Po 4 spaces...
|
NULL
|
-132346385557481895
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-d PhostormVIewINavicarecodeFV faVsco.js°9 JY-20676-delete-report-related-objectsProiectC) AutomatedReport.onp© AskAnythingPromptServiceTest.php> DVOphp helpers.phpC) InitialFrontendState.pnp© Jiminny.phpC Plan.pnp© Serializer.php© TeamScimDetails.php> @ bootstrap> O build>@ config|>CJ contrib|> @ database>O docsu troni-end>C) langnode modules library root> O phpstan> public>O resourcesv h routesphp ani.oho3hp api_v2.phpphp console.ohophp customer_api.phpphp embedded.phpphe health nhnpho seim nhnlphp uprotected_web.phpohp web.onephp webhook.php>@ scripts>@J storagevMtocte› O Feature> D Integration> D Services> D Stubsv D Unit> O Actionscomponent•DActionltems→DActivitv> M ActivitvAnalvticsM ActivitvSearch• MAiActi vitvivoeM AiAutomation>M AiCallScorind• M AckAnvthinglc SavedSearchCRUDFeaturelest.onp x©AskAnythingPromptDto.php© BaseService.phpC) RoleAttr.png© ScimProvisioning.phpC) ListenerRoleCa() RoleAttrTest.phpListenerRoleCannotHaveAdminOrManagerPermissionRule.phpC) Activity/Close/Service.php© Activity/RingCentral/Service.phpC) SyncActivity.php(C) Crm/Close/Service.phoInamesnace Tests Feature SavedSearches .Use ...22 Dfinal class SavedSearchCRUDFeatureTest extends TestCaseuse varabaselransactions27 D >#[\PHPUnit\Framework\Attributes\Test]public function userCanGetAList0fSavedSearches: void{...}104 D >#[\PHPUnit\Framework\Attributes\Testlpublic function userCanCreateASavedSearchBasedOnActivityFilters: void{...}194 07# PhPun1t Framework Attributes lestpublic function userCanUpdateASavedSearchBased0nActivityFilters@: void{...}257 0 >#|PHPUnit Framework Attributes Testpublic function userCanDeleteASavedSearchO: void{...}lAccent Reiect#PHPUnit Framework Attributes Test316 D>public function deletingSavedSearchDisablesRelatedReportsO: voids....c AckAnvthinaPromntServicetlect nhnC HistorvServiceTest nhn> M Ack liminnvAAccept File &+X Reject File 4%€+8 of 8 files →ries for other languages. (59 minutes ago)= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php X<?phpdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tysearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 ct ›146 @t>150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}"suppont Dally • In 4h 1m100% 5• Tue 19 May 10:59:58AskJiminnyReportActivityServiceTest vCascadeASK Jiminny Report+0 ..AskJiminnyReportsController.php+12 -1Thoughtsw6/7 tasks done• Add validation to toggleStatus to prevent enabling reports with null references• Write unit tests for new functionalitySearched SearchTest in testsSearched ASKAnythingPromptlest in testsSoarched Ack.liminnvRenorteControllerTect in tocteSearched Activity. Search in testsSearched ActivityControllerTest in testsThought for 1s›Read SavedSearchCRUDFeatureTest.ohdSearched AskJiminnyReportsController in testsThought for 1s>Ihought for 1s>Error while editina AskAnvthinaPromotServiceTest.ohoThouaht for 1s >error while editing ASKAnychingPromptservicelest.pnp ›Thought for 1s>• AskAnythingPromptServiceTest.php+1-1Inoughts• AskAnythingPromptServiceTest.php+1-1• SavedSearchCRUDFeatureTest.phpThe user has asked me to imolement loqic for handlina Ask Jiminny reports when related saved searches or ask-liminnvprompts are deleted. Let me review what l've done so far.1. Added has_reports attribute to Search model2. V Added has reports attribute to AskAnvthinaPromot model and DTO.3. VUodated saved search deletion to disable related renorts .4. 7 Updated ask-liminny prompt deletion to disable related reports* Reiect allAccent allAsk anvthing 884-D)« Code SWF-1.6WN Windsurf Teams214-1UTE.8Po 4 spaces...
|
56352
|
NULL
|
NULL
|
NULL
|
|
56352
|
1965
|
9
|
2026-05-19T07:59:55.359134+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779177595359_m2.jpg...
|
PhpStorm
|
faVsco.js – SavedSearchCRUDFeatureTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormVIeWINavicarecodeWindowmelpFV faVsco.js?9 PhpStormVIeWINavicarecodeWindowmelpFV faVsco.js?9 JY-20676-delete-report-related-objectsProiect(C) AutomatedReport.onp(C) Ask.JiminnyReportsController.png© AskAnythingPromptServiceTest.php> DVOphp helpers.phpC) InitialFrontendState.pnp© Jiminny.phpC Plan.pnp© Serializer.php© TeamScimDetails.php> @ bootstrap> O build>@ config|>CJ contrib|> @ database>O docssavedsearchekubreaturerest.onpx© AskAnythingPrompt.php©AskAnythingPromptDto.phpASKAnythingPromptservice.ong© CrmOwnerResolver.php© BaseService.php40) SoftPhoneManager.php©) Coreuserkequest.pnp© Constants.php© RoleAttr.php© ScimProvisioning.php© CoreUser.php() ListenerRoleCannotHaveAdminOrManagerPermissionRuleTest.php() RoleAttrTest.phpListenerRoleCannotHaveAdminOrManagerPermissionRule.php© Activity/Close/Service.php© Activity/RingCentral/Service.phpC) SyncActivity.php(C) Crm/Close/Service.pho(C) TextMessaqingService.phpu troni-end>C) langnode modules library root> O phostan> public>O resourcesv ul routecphp ani.oho3hp api_v2.phpphp console.ohophp customer abi.ohophp embedded.phpphe health nhnpho seim nhnlphp uprotected_web.phpohp web.onephp webhook.php>@ scripts>@J storagevMtocte>C Feature> C Integration> D Services> D Stubsv D Unit> O Actions0 Component•DActionltems→DActivitv> M ActivitvAnalvticsM ActivitvSearch• MAiActi vitvivoeM A Automation>M AiCallScorind• M AckAnvthinglc AckAnvthinaPromntServicetlect nhnC HistorvServiceTest nhn> M Ack liminnvAflof2edits &Accept File &+X Reject File 4%€+ 7 of 7 files →• M RillinaManadementely uninstall Hunspell without affecting the dictionaries for other languages. (59 minutes ago)= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php X<?phpdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 @t›146 @t>150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function__construct(UserRepository $userRepository)1...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}"suppont Dally • In 4h 1m100% 5• Tue 19 May 10:59:55U AskJiminnyReportActivityServiceTest vCascadeASK Jiminny ReportAskAnythingPromptService.phpInoughts5/7 tasks done• Update ask-jiminny prompt deletion to disable related reports6 Add validation to toaglestatus to prevent enabling reporis with null rererencesThought tor 1sAskJiminnvReportsController.phpThouahts6 7 tasks done• Add validation to toggleStatus to prevent enabling reports with null references• Write unit tests for new functionalitySearched SearchTest in testsThought for 1s>Searched Activity. Search in testssearched ActivityControllerTest in testsSearched AskAnvthina in testsRead SavedSearchCRUDFeatureTest.ohvlInought tor isRead. AskAnvthinaPromotServiceTest.ohvInougnt tor is)Frror while editina AskAnvthingPromntServiceTest.ohnThouaht for 1s)Error while editing AskAnythingPromptServiceTest.phpThought for 1c>• AskAnythingPromptServiceTest.phpThoughts• AskAnvthingPromptServiceTest.ohpSavedSearchCRUDFeatureTest.oho+0 ..+5 -2+12 -1+1-1+1 -11wAsk anvthina (884-L)+ @Code SWF-16* Reiect allAccent alliWN Windsurf Teamc228-1UTF.8io 4 spaces...
|
NULL
|
-3902596750252666978
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormVIeWINavicarecodeWindowmelpFV faVsco.js?9 PhpStormVIeWINavicarecodeWindowmelpFV faVsco.js?9 JY-20676-delete-report-related-objectsProiect(C) AutomatedReport.onp(C) Ask.JiminnyReportsController.png© AskAnythingPromptServiceTest.php> DVOphp helpers.phpC) InitialFrontendState.pnp© Jiminny.phpC Plan.pnp© Serializer.php© TeamScimDetails.php> @ bootstrap> O build>@ config|>CJ contrib|> @ database>O docssavedsearchekubreaturerest.onpx© AskAnythingPrompt.php©AskAnythingPromptDto.phpASKAnythingPromptservice.ong© CrmOwnerResolver.php© BaseService.php40) SoftPhoneManager.php©) Coreuserkequest.pnp© Constants.php© RoleAttr.php© ScimProvisioning.php© CoreUser.php() ListenerRoleCannotHaveAdminOrManagerPermissionRuleTest.php() RoleAttrTest.phpListenerRoleCannotHaveAdminOrManagerPermissionRule.php© Activity/Close/Service.php© Activity/RingCentral/Service.phpC) SyncActivity.php(C) Crm/Close/Service.pho(C) TextMessaqingService.phpu troni-end>C) langnode modules library root> O phostan> public>O resourcesv ul routecphp ani.oho3hp api_v2.phpphp console.ohophp customer abi.ohophp embedded.phpphe health nhnpho seim nhnlphp uprotected_web.phpohp web.onephp webhook.php>@ scripts>@J storagevMtocte>C Feature> C Integration> D Services> D Stubsv D Unit> O Actions0 Component•DActionltems→DActivitv> M ActivitvAnalvticsM ActivitvSearch• MAiActi vitvivoeM A Automation>M AiCallScorind• M AckAnvthinglc AckAnvthinaPromntServicetlect nhnC HistorvServiceTest nhn> M Ack liminnvAflof2edits &Accept File &+X Reject File 4%€+ 7 of 7 files →• M RillinaManadementely uninstall Hunspell without affecting the dictionaries for other languages. (59 minutes ago)= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php X<?phpdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 @t›146 @t>150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function__construct(UserRepository $userRepository)1...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}"suppont Dally • In 4h 1m100% 5• Tue 19 May 10:59:55U AskJiminnyReportActivityServiceTest vCascadeASK Jiminny ReportAskAnythingPromptService.phpInoughts5/7 tasks done• Update ask-jiminny prompt deletion to disable related reports6 Add validation to toaglestatus to prevent enabling reporis with null rererencesThought tor 1sAskJiminnvReportsController.phpThouahts6 7 tasks done• Add validation to toggleStatus to prevent enabling reports with null references• Write unit tests for new functionalitySearched SearchTest in testsThought for 1s>Searched Activity. Search in testssearched ActivityControllerTest in testsSearched AskAnvthina in testsRead SavedSearchCRUDFeatureTest.ohvlInought tor isRead. AskAnvthinaPromotServiceTest.ohvInougnt tor is)Frror while editina AskAnvthingPromntServiceTest.ohnThouaht for 1s)Error while editing AskAnythingPromptServiceTest.phpThought for 1c>• AskAnythingPromptServiceTest.phpThoughts• AskAnvthingPromptServiceTest.ohpSavedSearchCRUDFeatureTest.oho+0 ..+5 -2+12 -1+1-1+1 -11wAsk anvthina (884-L)+ @Code SWF-16* Reiect allAccent alliWN Windsurf Teamc228-1UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
65464
|
2307
|
8
|
2026-05-21T07:39:54.145510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349194145_m2.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>74 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6716744925979352142
|
-8204420343923495994
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
slackFV [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODCActivityController.ong© ConvertLeadActivities.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyLeadkepository.ong© PurgeLookupCache.php© SyncPlanhat.phpo synerorlannatpnpA console [STAGING]AcuivityLeadConverted.php© UserAutomatedreporscontroller.phpx© PlaybackController.php)Inteqrationapp/service.onoC) LeadConverted.ono570571C) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php572(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php573574© AutomatedReportResult.php(C) AutomatedReport.oho575use Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;A1422 ^ v 576use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...} -609610--611612613616Tx: AutovSo jiminny vCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN' (owner)' ELSE'' END) AS USEr_i041 A1 A40 V65 ^sa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociablle 1diJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * trom automated reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044|"pat", "podcast"SELECT * FROM automated report results WHERE uuid to bin( '822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * from automated_report_results order by id desc;SELECT * FROMWHERE id = 1919:select * fromautomated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Lesg': # 711. 692. [EMAIL] * from playbooks where team_id = 711; # event 226147SELEd * FROM Dlavbook catedonies WHERE Dlavbook 1d = 55151SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * SROM eom fields WHEREI1d = 2261471SELECT * FROM crm_field_values WHERE crm_field_id = 226147;SELECT * FROM crm_configurations WHERE id = 692;SELSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '• END) AS user_id,v.emailsa.*,t.owner id FROM social accounts saJOIN users u on v.id = sa.sociable idJOIN teams t 1..n<->1: on t.id = u.team idWHERE U.team_id = 711 and sa.provider = 'salesforce";SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsSELECTS 0 hllsupoont Dally • In 4h 40m100% 52• Inu z1 May 10.10.34AskJiminnyReportActivityServiceTestvCascadePlaybook Categorie+0 ..connected. How isi. Can we vali eoneactivity tyee nith orit cabest aeeiewmane o, W dati anwsyggesinsingrow hohow to distinguishThoughts68 Fast Context How are playbook_categories populated and synced from crm_fields / crm_field_values... in 4.14s >Explored 4 files and searched 5 queriesRead ImportActivitvTypes.php #L95-104Searched olavbook cateaories in databaselmiarations (4) >egories|playbook_categories.*create in database/migrations (0)searched playoook_categor in database/migrations (<)Searched activity type in database/migrations (4)SurtingAsk anvthina (*4L)@ Codo Caudo Onue 17 ModiumNN Windeurf Toame 504.60...
|
65462
|
NULL
|
NULL
|
NULL
|
|
65463
|
2306
|
12
|
2026-05-21T07:39:54.167425+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349194167_m1.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>74 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"11","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"130","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":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}]...
|
-5730062760152755435
|
-7851939513083130939
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
...
|
65461
|
NULL
|
NULL
|
NULL
|
|
65459
|
2307
|
6
|
2026-05-21T07:39:39.307259+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349179307_m2.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>74 incoming commits<br/>","role_description":"button","is_enabled":true,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
slackFV [EMA Project: faVsco.js, menu
master, menu
slackFV [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODCActivityController.ong© ConvertLeadActivities.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyLeadkepository.ong© PurgeLookupCache.php© SyncPlanhat.phpo synerorlannatpnpA console [STAGING]AcuivityLeadConverted.php© UserAutomatedreporscontroller.phpx© PlaybackController.php)Inteqrationapp/service.onoC) LeadConverted.ono570571C) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php572(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php573574© AutomatedReportResult.php(C) AutomatedReport.oho575use Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;A1422 ^ v 576use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...} -609610--611612613616Tx: AutovSo jiminny vCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN' (owner)' ELSE'' END) AS USEr_i041 A1 A40 V65 ^sa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociablle 1diJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * trom automated reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044|"pat", "podcast"SELECT * FROM automated report results WHERE uuid to bin( '822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * from automated_report_results order by id desc;SELECT * FROMWHERE id = 1919:select * fromautomated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Lesg': # 711. 692. [EMAIL] * from playbooks where team_id = 711; # event 226147SELEd * FROM Dlavbook catedonies WHERE Dlavbook 1d = 55151SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * SROM eom fields WHEREI1d = 2261471SELECT * FROM crm_field_values WHERE crm_field_id = 226147;SELECT * FROM crm_configurations WHERE id = 692;SELSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '• END) AS user_id,v.emailsa.*,t.owner id FROM social accounts saJOIN users u on v.id = sa.sociable idJOIN teams t 1..n<->1: on t.id = u.team idWHERE U.team_id = 711 and sa.provider = 'salesforce";SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsSELECTS 0 hllsupoont Dally • In 4h 40m100% 52• Inu z1 May 10.10.34AskJiminnyReportActivityServiceTestvCascadePlaybook Categorie+0 ..connected. How isi. Can we vali eoneactivity tyee nith orit cabest aeeiewmane o, W dati anwsyggesinsingrow hohow to distinguishThoughts68 Fast Context How are playbook_categories populated and synced from crm_fields / crm_field_values... in 4.14s >Explored 4 files and searched 5 queriesRead ImportActivitvTypes.php #L95-104Searched olavbook cateaories in databaselmiarations (4) >egories|playbook_categories.*create in database/migrations (0)searched playoook_categor in database/migrations (<)Searched activity type in database/migrations (4)SurtingAsk anvthina (*4L)@ Codo Caudo Onue 17 ModiumNN Windeurf Toame 504.60...
|
65455
|
NULL
|
NULL
|
NULL
|
|
65457
|
2306
|
8
|
2026-05-21T07:39:39.281963+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349179281_m1.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>74 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-943456739835505562
|
-8636494053633454656
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
SlackFileEditViewGoHistoryWindowHelpscreelDOCKER0 81DEV (-zsh)₴2APP (-zsh)whisper_init_state:whisper_init_state:kv padsize=2.36 MBcompute buffer (conv)whisper_init_state: compute buffer (encode) =whisper_init_state: compute buffer (cross)=whisper_init_state:compute buffer (decode) =14.17 MB65.96 MB8.50 MB96.83 MBggml_metal_free: deallocatingwhisper_backend_init_gpu: device 0: Metal (type:1)whisper_backend_init_gpu: found GPU device 0: Metal (type: 1, cnt: 0)whisper_backend_init_gpu: using Metal backendggml_metal_init: allocatingggml_metal_init: found device: Apple M1ggml_metal_init: picking default device: Apple M1ggml_metal_init:use fusion= trueggml_metal_init:use concurrency= trueggml_metal_init: use graph optimizetruewhisper_backend_init: using BLAS backendwhisper_init_state: kv self size3.15 MBwhisper_init_state: kv cross size =9.44 MBwhisper_init_state: kv padsize =2.36 MBwhisper_init_state: compute buffer (conv)whisper_init_state: compute buffer (encode) =whisper_init_state: compute buffer (cross)=whisper_init_state: compute buffer (decode) =14.17 MB65.96 MB8.50 MB96.83 MBggml_metal_free: deallocatingwhisper_backend_init_gpu:device 0: Metal (type: 1)whisper_backend_init_gpu: found GPUdevice 0: Metal (type: 1, cnt: 0)whisper_backend_init_gpu: using Metal backendggml_metal_init: allocatingggml_metal_init: found device: Apple M1ggml_metal_init: picking default device: Apple M1ggml_metal_init:use fusion= trueggml_metal_init:use concurrencytrueggml_metal_init:use graph optimizetruewhisper_backend_init: using BLAS backendwhisper_init_state: kv self size3.15 MBwhisper_init_state: kv cross size =9.44 MBwhisper_init_state: kv padsize=2.36 MBwhisper_init_state: compute buffer (conv)whisper_init_state: compute buffer (encode) =whisper_init_state: compute buffer (cross)whisper_init_state: compute buffer (decode) =14.17 MB65.96 MB8.50 MB96.83 MBggml_metal_free: deallocating2026-05-21T10:13:41.913005ZINFO screenpipe_audio::audio_manager::manager: reconciliatior2026-05-21710:15:15.5768762INFO screenpipe_engine::event_driven_capture: content dedup:j Support Daily - in 4h 45 m100% С8• Thu 21 May 10:15:32•••EDQHomeDMsActivityFilesLater•••More+→Describe what you are looking forJiminny ...ThreadsC9 Unreadsà platform-inner-teamGalya Dimitrova, Nikolay Yankov, and youa ThreadsHuddles• Drafts & sentDirectories01External connections* Starred& jiminny-x-integrati...platform-inner-teamChannels# ai-chapter# alerts# backend# bugs# confusion-clinic# curiosity_lab# engineering# general# happy_birthday# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# supportLukas Kovalik 31 minutes agoдобро утро, тази страница не трябва ли да япоказваме само ако няма reportsCleanShot 2026-05-21 at [EMAIL] your questions into ongoing insightsGalya Dimitrova 23 minutes agoДа, би трябвалоLukas Kovalik 22 minutes agoче сега ми го показва и ако имам reportLukas Kovalik 21 minutes agoна neptuneNewNikolay Yankov 6 minutes agoвсичко е наредReply...Also send to à platform-inner-team+AaYou're up to date...
|
65456
|
NULL
|
NULL
|
NULL
|
|
65456
|
2306
|
7
|
2026-05-21T07:39:36.698436+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349176698_m1.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>74 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"11","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"130","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): Account\n {\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): Contact\n {\n $account = null;\n // Contacts may not have accounts...\n if (isset($crmData['AccountId'])) {\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry']);\n\n if ($countryCode === null && $account) {\n $countryCode = $account->country_code;\n }\n }\n\n $ext = null;\n $parsedNumber = [];\n if (empty($crmData['Phone']) === false) {\n $number = Str::limit($crmData['Phone'], 25, '');\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n if (empty($parsedNumber['ext']) === false) {\n $ext = Str::limit($parsedNumber['ext'], 10, '');\n }\n }\n\n $mobileNumber = null;\n if (empty($crmData['MobilePhone']) === false) {\n $mobileNumber = Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . (new DecorateActivity())->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Helpers;\n\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\n\n/**\n * A collection of DB-related internal functionalities or all CRMs.\n */\nclass CrmHelperRepository\n{\n public function getPlaybookCategory(Playbook $playbook, string $categoryName): ?PlaybookCategory\n {\n /** @var ?PlaybookCategory */\n return $playbook->categories()\n ->where('name', trim($categoryName))\n ->orderBy('id', 'desc')\n ->first();\n }\n\n public function getActivityFieldValueByName(Field $activityField, string $categoryName): ?FieldValue\n {\n /** @var ?FieldValue */\n return $activityField->values()\n ->where('label', $categoryName)\n ->first();\n }\n\n public function existsActivityFieldValueByName(Field $activityField, string $categoryName): bool\n {\n return $activityField->values()\n ->where('label', $categoryName)\n ->exists();\n }\n\n public function hasFeature(Activity $activity, FeatureEnum $featureName): bool\n {\n return $activity->getUser()\n ->getTeam()\n ->hasFeature($featureName);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Helpers;\n\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\n\n/**\n * A collection of DB-related internal functionalities or all CRMs.\n */\nclass CrmHelperRepository\n{\n public function getPlaybookCategory(Playbook $playbook, string $categoryName): ?PlaybookCategory\n {\n /** @var ?PlaybookCategory */\n return $playbook->categories()\n ->where('name', trim($categoryName))\n ->orderBy('id', 'desc')\n ->first();\n }\n\n public function getActivityFieldValueByName(Field $activityField, string $categoryName): ?FieldValue\n {\n /** @var ?FieldValue */\n return $activityField->values()\n ->where('label', $categoryName)\n ->first();\n }\n\n public function existsActivityFieldValueByName(Field $activityField, string $categoryName): bool\n {\n return $activityField->values()\n ->where('label', $categoryName)\n ->exists();\n }\n\n public function hasFeature(Activity $activity, FeatureEnum $featureName): bool\n {\n return $activity->getUser()\n ->getTeam()\n ->hasFeature($featureName);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5730062760152755435
|
-7851939513083130939
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
11
130
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): Account
{
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['StageName'])) {
$stage = $this->config
->stages()
->where('name', $crmData['StageName'])
->where('type', Stage::TYPE_OPPORTUNITY)
->orderBy('is_selectable', 'DESC')
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
65455
|
2307
|
5
|
2026-05-21T07:39:35.083426+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-21/1779 /Users/lukas/.screenpipe/data/data/2026-05-21/1779349175083_m2.jpg...
|
PhpStorm
|
faVsco.js – Salesforce/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
slackFV [EMAIL]=.phounit.result.cacheE .prettierig slackFV [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODCActivityController.ong© ConvertLeadActivities.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyLeadkepository.ong© PurgeLookupCache.php© SyncPlanhat.phpo synerorlannatpnpA console [STAGING]AcuivityLeadConverted.php© UserAutomatedreporscontroller.phpx© PlaybackController.php)Inteqrationapp/service.onoC) LeadConverted.ono570571C) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php572(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php573574© AutomatedReportResult.php(C) AutomatedReport.oho575use Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;A1422 ^ v 576use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...} -609610--611612613616Tx: AutovSo jiminny vCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN' (owner)' ELSE'' END) AS USEr_i041 A1 A40 V65 ^sa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociablle 1diJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * trom automated reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044|"pat", "podcast"SELECT * FROM automated report results WHERE uuid to bin( '822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * from automated_report_results order by id desc;SELECT * FROMWHERE id = 1919:select * fromautomated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Lesg': # 711. 692. [EMAIL] * from playbooks where team_id = 711; # event 226147SELEd * FROM Dlavbook catedonies WHERE Dlavbook 1d = 55151SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * SROM eom fields WHEREI1d = 2261471SELECT * FROM crm_field_values WHERE crm_field_id = 226147;SELECT * FROM crm_configurations WHERE id = 692;SELSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '• END) AS user_id,v.emailsa.*,t.owner id FROM social accounts saJOIN users u on v.id = sa.sociable idJOIN teams t 1..n<->1: on t.id = u.team idWHERE U.team_id = 711 and sa.provider = 'salesforce";SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsSELECTS 0 hllsupoont Dally • In 4h 40m100% 52• Inu z1 May 10.10.34AskJiminnyReportActivityServiceTestvCascadePlaybook Categorie+0 ..connected. How isi. Can we vali eoneactivity tyee nith orit cabest aeeiewmane o, W dati anwsyggesinsingrow hohow to distinguishThoughts68 Fast Context How are playbook_categories populated and synced from crm_fields / crm_field_values... in 4.14s >Explored 4 files and searched 5 queriesRead ImportActivitvTypes.php #L95-104Searched olavbook cateaories in databaselmiarations (4) >egories|playbook_categories.*create in database/migrations (0)searched playoook_categor in database/migrations (<)Searched activity type in database/migrations (4)SurtingAsk anvthina (*4L)@ Codo Caudo Onue 17 ModiumNN Windeurf Toame 504.60...
|
NULL
|
4726481696127267389
|
NULL
|
click
|
ocr
|
NULL
|
slackFV [EMAIL]=.phounit.result.cacheE .prettierig slackFV [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODCActivityController.ong© ConvertLeadActivities.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyLeadkepository.ong© PurgeLookupCache.php© SyncPlanhat.phpo synerorlannatpnpA console [STAGING]AcuivityLeadConverted.php© UserAutomatedreporscontroller.phpx© PlaybackController.php)Inteqrationapp/service.onoC) LeadConverted.ono570571C) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php572(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php573574© AutomatedReportResult.php(C) AutomatedReport.oho575use Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;A1422 ^ v 576use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...} -609610--611612613616Tx: AutovSo jiminny vCONCAT(u.id, CASE WHEN U.id = t.owner_id THEN' (owner)' ELSE'' END) AS USEr_i041 A1 A40 V65 ^sa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociablle 1diJOIN teams t 1..n<->1: on t.id = u.team_idWHERE u.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * trom automated reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044|"pat", "podcast"SELECT * FROM automated report results WHERE uuid to bin( '822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * from automated_report_results order by id desc;SELECT * FROMWHERE id = 1919:select * fromautomated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Lesg': # 711. 692. [EMAIL] * from playbooks where team_id = 711; # event 226147SELEd * FROM Dlavbook catedonies WHERE Dlavbook 1d = 55151SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * SROM eom fields WHEREI1d = 2261471SELECT * FROM crm_field_values WHERE crm_field_id = 226147;SELECT * FROM crm_configurations WHERE id = 692;SELSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '• END) AS user_id,v.emailsa.*,t.owner id FROM social accounts saJOIN users u on v.id = sa.sociable idJOIN teams t 1..n<->1: on t.id = u.team idWHERE U.team_id = 711 and sa.provider = 'salesforce";SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsSELECTS 0 hllsupoont Dally • In 4h 40m100% 52• Inu z1 May 10.10.34AskJiminnyReportActivityServiceTestvCascadePlaybook Categorie+0 ..connected. How isi. Can we vali eoneactivity tyee nith orit cabest aeeiewmane o, W dati anwsyggesinsingrow hohow to distinguishThoughts68 Fast Context How are playbook_categories populated and synced from crm_fields / crm_field_values... in 4.14s >Explored 4 files and searched 5 queriesRead ImportActivitvTypes.php #L95-104Searched olavbook cateaories in databaselmiarations (4) >egories|playbook_categories.*create in database/migrations (0)searched playoook_categor in database/migrations (<)Searched activity type in database/migrations (4)SurtingAsk anvthina (*4L)@ Codo Caudo Onue 17 ModiumNN Windeurf Toame 504.60...
|
NULL
|
NULL
|
NULL
|
NULL
|