|
70915
|
2547
|
2
|
2026-05-22T10:50:10.006617+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447010006_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70907
|
NULL
|
NULL
|
NULL
|
|
70916
|
2546
|
3
|
2026-05-22T10:50:39.320802+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447039320_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70912
|
NULL
|
NULL
|
NULL
|
|
70917
|
2547
|
3
|
2026-05-22T10:50:40.470650+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447040470_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70907
|
NULL
|
NULL
|
NULL
|
|
70918
|
2546
|
4
|
2026-05-22T10:51:09.737203+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447069737_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70912
|
NULL
|
NULL
|
NULL
|
|
70919
|
2547
|
4
|
2026-05-22T10:51:10.854869+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447070854_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70907
|
NULL
|
NULL
|
NULL
|
|
70920
|
2546
|
5
|
2026-05-22T10:51:35.284824+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447095284_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete modecreate mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable,useitat yourownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLaterMore+ED→Jiminny ...& jiminny-x-integrati...& platform-inner-teamE Channels# ai-chapter# alerts# backend# bugs# confusion-clinic# curiosity _lab# engineering# general# happy_birthday# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...0 Direct messagesGalya Dimitrova "o Nikolay YankovP. Aneliya Angelova. Stoyan Tanevdo James Graham> 0100% С8 • Fri 22 May 13:51:34Describe what you are looking for# releases8 22•MessagesC Files• Bookmarks+fetch PR data vi- CitLlnh API0f883d21 - RevToday~orking version1be25ca1 - Add github_token to Claude Codeaction for enhanced authenticationShow more( jiminny/app| Added by GitHubNewCircleCl APP 1:26 PMDeployment Successful!Project: appWhen:05/22/202610:26:05Tag:View JobGitHub APP 1:27 PM4 new commits pushed to master byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app| Added by GitHubMessage #releases+..•...
|
NULL
|
6049801230985249792
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete modecreate mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable,useitat yourownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLaterMore+ED→Jiminny ...& jiminny-x-integrati...& platform-inner-teamE Channels# ai-chapter# alerts# backend# bugs# confusion-clinic# curiosity _lab# engineering# general# happy_birthday# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...0 Direct messagesGalya Dimitrova "o Nikolay YankovP. Aneliya Angelova. Stoyan Tanevdo James Graham> 0100% С8 • Fri 22 May 13:51:34Describe what you are looking for# releases8 22•MessagesC Files• Bookmarks+fetch PR data vi- CitLlnh API0f883d21 - RevToday~orking version1be25ca1 - Add github_token to Claude Codeaction for enhanced authenticationShow more( jiminny/app| Added by GitHubNewCircleCl APP 1:26 PMDeployment Successful!Project: appWhen:05/22/202610:26:05Tag:View JobGitHub APP 1:27 PM4 new commits pushed to master byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app| Added by GitHubMessage #releases+..•...
|
70912
|
NULL
|
NULL
|
NULL
|
|
70921
|
2547
|
5
|
2026-05-22T10:51:35.398563+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447095398_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows AoplicationExceotion580>G-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponsel...}Local changesShelfConsole xLogx +env.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+ → @ Side-by-side viewer ~Do not ianore v81aee7aad config/logging.phpColalalallale walalewalat© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')l1.C) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBộ ?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilion ient = nulupublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 2. Fri 22 May 13:51:35AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ask anything (#tL)1 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/Laravel.log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf TeamcPo 4 spaces...
|
NULL
|
-2125699717896779186
|
NULL
|
click
|
ocr
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows AoplicationExceotion580>G-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponsel...}Local changesShelfConsole xLogx +env.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+ → @ Side-by-side viewer ~Do not ianore v81aee7aad config/logging.phpColalalallale walalewalat© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')l1.C) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBộ ?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilion ient = nulupublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 2. Fri 22 May 13:51:35AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ask anything (#tL)1 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/Laravel.log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf TeamcPo 4 spaces...
|
70907
|
NULL
|
NULL
|
NULL
|
|
70922
|
2546
|
6
|
2026-05-22T10:52:06.945456+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447126945_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70923
|
2547
|
6
|
2026-05-22T10:52:07.569010+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447127569_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70924
|
2546
|
7
|
2026-05-22T10:52:37.473258+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447157473_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70925
|
2547
|
7
|
2026-05-22T10:52:38.061730+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447158061_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70926
|
2546
|
8
|
2026-05-22T10:53:07.867435+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447187867_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70927
|
2547
|
8
|
2026-05-22T10:53:08.508354+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447188508_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70928
|
2546
|
9
|
2026-05-22T10:53:38.272950+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447218272_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70929
|
2547
|
9
|
2026-05-22T10:53:39.025130+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447219025_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70930
|
NULL
|
0
|
2026-05-22T10:54:08.589011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447248589_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70931
|
NULL
|
0
|
2026-05-22T10:54:09.454538+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447249454_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70932
|
2548
|
0
|
2026-05-22T10:54:39.045212+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447279045_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70933
|
2549
|
0
|
2026-05-22T10:54:40.010767+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447280010_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70934
|
2548
|
1
|
2026-05-22T10:55:09.481538+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447309481_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70935
|
2549
|
1
|
2026-05-22T10:55:10.418077+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447310418_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70936
|
2548
|
2
|
2026-05-22T10:55:39.865435+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447339865_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70937
|
2549
|
2
|
2026-05-22T10:55:40.912316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447340912_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70938
|
2548
|
3
|
2026-05-22T10:56:10.286364+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447370286_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70939
|
2549
|
3
|
2026-05-22T10:56:11.457270+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447371457_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70940
|
2548
|
4
|
2026-05-22T10:56:40.651098+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447400651_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70941
|
2549
|
4
|
2026-05-22T10:56:42.002072+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447402002_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70942
|
2548
|
5
|
2026-05-22T10:57:11.063412+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447431063_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70943
|
2549
|
5
|
2026-05-22T10:57:12.457724+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447432457_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70944
|
2548
|
6
|
2026-05-22T10:57:41.505775+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447461505_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70945
|
2549
|
6
|
2026-05-22T10:57:42.855058+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447462855_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70923
|
NULL
|
NULL
|
NULL
|
|
70946
|
2548
|
7
|
2026-05-22T10:58:09.609737+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447489609_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
SlackFileEditViewGoHistor Project: faVsco.js, menu
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete modecreate mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, useat yourownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used• 0HomeDMsActivityFilesLaterMore+ED→Jiminny ...& jiminny-x-integrati...& platform-inner-team# Channels# the_people_of_...# ai-chapter# alerts# backend# bugs# confusion-clinic# curiosity_lab# engineering# general# happy_birthday# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous0 Direct messagesGalya Dimitrova *o Nikolay YankovP. Aneliya Angelova. Stoyan Tanevdo James Graham> 0(all100% С8 • Fri 22 May 13:58:09Describe what you are looking for# releases8 226 0• MessagesProject: aprC FilesBookmarks+3 new messages10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
70922
|
NULL
|
NULL
|
NULL
|
|
70956
|
NULL
|
0
|
2026-05-22T10:59:04.342999+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447544342_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70954
|
NULL
|
NULL
|
NULL
|
|
70957
|
NULL
|
0
|
2026-05-22T10:59:19.116300+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447559116_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70955
|
NULL
|
NULL
|
NULL
|
|
70958
|
2551
|
0
|
2026-05-22T10:59:35.297966+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447575297_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70959
|
2550
|
0
|
2026-05-22T10:59:49.665879+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447589665_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70960
|
2551
|
1
|
2026-05-22T11:00:05.736658+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447605736_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70958
|
NULL
|
NULL
|
NULL
|
|
70968
|
2550
|
5
|
2026-05-22T11:00:39.361308+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447639361_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete mode100644 app/Component/ES/Processor/Traits/WorkerIdTrait.phpcreate mode 100644 app/Component/ES/QueuePriorityEnum.phpTrginusWorker/aTrai+: Phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, use itat your ownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLater+ED→Jiminny ...& 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# support# thank-yous# the_people_of jimi...^ Direct messagesP. Galya Dimitrova ". Nikolay YankovP. Aneliya Angelova2 Stoyan Tanevd James Graham(all100% С8 • Fri 22 May 14:00:39Describe what you are looking for# releases8 226 0• MessagesProject: appC FilesBookmarksM/kan.Today ~J26+10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubNewCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
NULL
|
-1370887085875687020
|
NULL
|
visual_change
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete mode100644 app/Component/ES/Processor/Traits/WorkerIdTrait.phpcreate mode 100644 app/Component/ES/QueuePriorityEnum.phpTrginusWorker/aTrai+: Phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, use itat your ownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLater+ED→Jiminny ...& 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# support# thank-yous# the_people_of jimi...^ Direct messagesP. Galya Dimitrova ". Nikolay YankovP. Aneliya Angelova2 Stoyan Tanevd James Graham(all100% С8 • Fri 22 May 14:00:39Describe what you are looking for# releases8 226 0• MessagesProject: appC FilesBookmarksM/kan.Today ~J26+10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubNewCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
70967
|
NULL
|
NULL
|
NULL
|
|
70969
|
2551
|
5
|
2026-05-22T11:00:40.735804+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447640735_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4KLAY70 (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotion580>GsTaoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponsel...}Local changesShelfConsole xLogx +venv.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+→ a.Side-bv-cide viewer -Do not ianore v81aee7aad config/logging.phpphp logging.php config© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')lC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBB|?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilion ient = nulupublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 52. Fri 22 May 14:00:40AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..A22 V1V1AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ask anything (&OL)+ <> Code SWE-1.61 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/laravel.Log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf Teamcio 4 spaces...
|
NULL
|
4687728746416176036
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4KLAY70 (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotion580>GsTaoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponsel...}Local changesShelfConsole xLogx +venv.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+→ a.Side-bv-cide viewer -Do not ianore v81aee7aad config/logging.phpphp logging.php config© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')lC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBB|?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilion ient = nulupublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 52. Fri 22 May 14:00:40AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..A22 V1V1AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ask anything (&OL)+ <> Code SWE-1.61 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/laravel.Log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf Teamcio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70970
|
2550
|
6
|
2026-05-22T11:00:41.575874+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447641575_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
SlackFileEditViewGoHistor Project: faVsco.js, menu
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete mode100644 app/Component/ES/Processor/Traits/WorkerIdTrait.phpcreate mode 100644 app/Component/ES/QueuePriorityEnum.phpTrginusWorker/aTrai+: Phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, use itat your ownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLater+ED→Jiminny ...& 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# support# thank-yous# the_people_of jimi...^ Direct messagesP. Galya Dimitrova ". Nikolay YankovP. Aneliya Angelova. Stoyan Tanev& James Graham> 0(all100% С8 • Fri 22 May 14:00:41Describe what you are looking for# releases8 226 0• MessagesProject: appC FilesBookmarksM/kan.Today ~J26+10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubNewCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70971
|
2551
|
6
|
2026-05-22T11:00:44.893573+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447644893_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}...
|
[{"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/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8419801864614017883
|
-4357855742655358964
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}...
|
70969
|
NULL
|
NULL
|
NULL
|
|
70972
|
2551
|
7
|
2026-05-22T11:00:54.022287+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447654022_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhn© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapzkesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotion5814>GsTaoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponset...*Local changesShelfConsole xLogx +env.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+→ a.Side-bv-cide viewer -Do not ianore v81aee7aad config/logging.phpphp logging.php config© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')lC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBộ ?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilionlient = nultpublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 2. Fri 22 May 14:00:53AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ack anvthing 18eA1)1 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/laravel.Log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf Teamcio 4 spaces...
|
NULL
|
-342719461974700732
|
NULL
|
click
|
ocr
|
NULL
|
PhostormFV faVsco.jsroledeyConvertLeadActivities.p PhostormFV faVsco.jsroledeyConvertLeadActivities.php© InsightsService.php© NumberAllocator.phpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhn© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsserviceprivate readonly Apikesponseservice sapzkesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotion5814>GsTaoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidh123 19)public function delete(Request Srequest, string $uuid): JsonResponset...*Local changesShelfConsole xLogx +env.local aon@.liminnvDehuaCommandinhn annlConcole/Commandd+→ a.Side-bv-cide viewer -Do not ianore v81aee7aad config/logging.phpphp logging.php config© SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManac© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app"driver' => 'errorlog'..'Level' => env('LOG_LEVEL', 'info'),"nath' => stonade nath('oas/anavel.iog')lC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appXBộ ?=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilionlient = nultpublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel100% 2. Fri 22 May 14:00:53AskJiminnyReportActivityServiceTest v& console [PROD]CascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnskealoneThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .Ack anvthing 18eA1)1 differenco"level' => envC'LOG LEVEL', Cinfo").'path' => storage_path('logs/laravel.Log'),"custom channelt =>fdnivent => Isinale!!'path' => storage_path('logs/custom.log'),'level' => env('LOG_LEVEL', 'info').W Windsurf Teamcio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70973
|
2551
|
8
|
2026-05-22T11:00:58.020012+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447658020_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
PhostormINav Project: faVsco.js, menu
master, menu
PhostormINavicarecodeFV faVsco.js°9 master kroledey© InsightsService.php© NumberAllocator.php1extmessagingservice.pngc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php© IpapiService.php© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.php© ResolveTeamCrmConnection.php© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.php123 19)c) Userservice.oneLocal changesShelfConsole xLogx +venv.local aon@.liminnvDehuaCommandinhn annlConcole/Commanddphp logging.php config9 SoftDhoneMenaaer nhn anniCon© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal appC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY70 (61pubulc tunccion -_conscrucelprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsservice.private readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackinterestReouest Srequest: JsonResponse-...* othrows ApplicationExceotionG-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidpublic function delete(Request Srequest, string $uuid): JsonResponset...*Do not ianore vHighlight words 13 g ?81aee7aad app/Compuse Jiminny\Services\Telephony\TwilioClientBuilder:use Psr\Log\ LoggerInterface:sconcact = null$stage = null;Sopportunity = null:ScrmService = Sthis->crmProviderRegistry->get(Steam->crm->provider):if (4 Suser->isCrmRequired0) {ScrmService->setUser(suser->team->owner100% 2. Fri 22 May 14:00:57AskJiminnyReportActivityServiceTest v=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]Cascade© TextMessagingService.php X A console [EU]A console [STAGING]Investigate Foreign KeSalestorce Token Fallb+0 ..stohe122 V1 V1 AVThe file is still off lot of warning just see what is wrongnamespace Jininny services lelephonyThoughts>use ...class TextMessaqngServiceThought for 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMes5 usagesorivate ?Team steam = nuluThoughts3 usadesnckcalonorivate ?TwilioClient stwilionlient = nuuuThoughtsFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added thepublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGenerator,private readonly TwilioClientBuilder $twilioClientBuilder,1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern* Othrows ConfigurationException• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "Unknownpublic function setTeam(Team $team): voidGal .sth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows ExceotzonAsk anything (dtloublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancel2 differencascurrent versionuse Jiminny\Services Telephony \TwilioClientBuilder:use Psr\Log\LoggerInterface:AuliminnvlComnonent|TwiliclConference|ConferenceManader SoftPhoneManaders create@utboundActivitvl TAJAScontact = null:$stage = null;Sopportunity = null:try {ScrmResolver = app(Crm0wnerResolver::class'team' => Steam.'providerslug => $team->getcrmConfiguration()->getProviderName)W Windsurf Teamc1-1UTE.8Po 4 spaces...
|
70972
|
NULL
|
NULL
|
NULL
|
|
70974
|
2551
|
9
|
2026-05-22T11:01:00.701050+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447660701_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70975
|
2551
|
10
|
2026-05-22T11:01:03.868711+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447663868_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}...
|
[{"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/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8419801864614017883
|
-4357855742655358964
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}...
|
70974
|
NULL
|
NULL
|
NULL
|
|
70976
|
2551
|
11
|
2026-05-22T11:01:11.022118+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447671022_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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...
|
[{"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/>Some incoming commits are not fetched<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}]...
|
5222591262487654398
|
-8636637129462003327
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
PhostormINavicarecodeFV faVsco.js°9 master kroledey© InsightsService.php© NumberAllocator.php1extmessagingservice.pngc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.php@) PocolveTeamCrmConnection.onp© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.phpc Userservice.onrsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscrucelprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsservice.private readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackinterestRequest Srequest: JsonResponse ...* othrows ApplicationExceotionG-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuid123 19)public function delete(Request Srequest, string $uuid): JsonResponset...*Local changesShelfConsole xLog Xenv.local aon@.liminnvDehuaCommandinhn ann/ConcolelCommandeDo not ianore vHighlight words 13 g ?php logging.php configg SoftPhoneManager.php app/Component/Twilio/Conference/Conferenceiane© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal app81aee7aad app/Compuse Jiminny\Services\Telephony\TwilioClientBuilder:use Psr\Log\ LoggerInterface:sconcact = null$stage = null;Sopportunity = null:ScrmService = Sthis->crmProviderReqistry->get(Steam->crm->provider):if (4 Suser->isCrmRequired0) {C) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appScrmService->setUser(suser->team->owner=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngService5 usagesorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilion ient = nulupublic function __construct(private readonly ProviderRegistry SproviderRegistry.private readonly ActivityService SactivityService,private readonly Webhook SurlGenerator,private readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* dchrows Exceotzonoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancelcurrent versionuse Jiminny\Services\Telephony TwilioClientBuilder:use Psr\Log\LoggerInterface:(Jiminny|Component\Twilio\Conference\ConferenceManager > SoftPhoneManager > createOutboundActivity() ~Scontact = null:$stage = null;Sopportunity = null:try {ScrmResolver = app(Crm0wnerResolver::class.'team' => Steam.'integrationAdmin' => $usen'providerslug => $team->getcrmConfiguration(->getProviderName100% 52• Fri 22 May 14:01:10AskJiminnyReportActivityServiceTest vCascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structure• First tryAdantive #S• If that fai Automatioallu halaneee aualitu and cocttoken from the organizationnt pattern• Let existitTokenInvalidException by settina CRM data to nullThis ensures Claude Opus 4.7 Mediunken when the admin token eynires nreventing "UnknownGPT-5.5 Low ThinkingClaude Opus 4.6 ThinkingClaude Sonnet 4.6 ThinkingSWE-1.6 FastAsk anything Start With History2 differencasW Windsurf Teamc1-1.UTE.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70977
|
2550
|
7
|
2026-05-22T11:01:14.775563+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447674775_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70970
|
NULL
|
NULL
|
NULL
|
|
70978
|
2551
|
12
|
2026-05-22T11:01:36.066090+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447696066_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"22","depth":4,"bounds":{"left":0.6911569,"top":0.10055866,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.703125,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\n }\n}","depth":4,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.36269948,"height":0.90263367},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Telephony;\n\nuse Jiminny\\Component\\Activity\\Services\\GetDefaultActivityTypeService;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance;\n\nclass TextMessagingService\n{\n private ?Team $team = null;\n private ?TwilioClient $twilioClient = null;\n\n public function __construct(\n private readonly ProviderRegistry $providerRegistry,\n private readonly ActivityService $activityService,\n private readonly Webhook $urlGenerator,\n private readonly TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n /**\n * @throws ConfigurationException\n */\n public function setTeam(Team $team): void\n {\n $this->team = $team;\n $this->twilioClient = $this->twilioClientBuilder->build($team);\n }\n\n /**\n * @throws \\Exception\n */\n public function send(User $from, string $to, string $message): MessageInstance\n {\n if ($this->team->id !== $from->team_id) {\n throw new \\InvalidArgumentException('Sender does not belong to this team.');\n }\n\n $payload = [\n 'from' => $from->softphone_number,\n 'body' => $message,\n 'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),\n ];\n\n if ($this->team->twilio_messaging_sid !== null) {\n $payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;\n } else {\n $payload['applicationSid'] = $this->team->twilio_sms_sid;\n }\n\n return $this->twilioClient\n ->messages\n ->create($to, $payload);\n }\n\n /**\n * @throws TwilioException\n */\n public function redact(Activity $activity): void\n {\n $this->twilioClient\n ->messages($activity->telephony_provider_id)\n ->delete();\n }\n\n public function buildActivity(\n ?string $messageId,\n string $type,\n User $user,\n string $from,\n string $to,\n string $message,\n ?string $customerId,\n ?string $objectType,\n ?string $countryCode\n ): Activity {\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n try {\n if ($customerId) {\n // Query the CRM for full details of this customer.\n [$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);\n\n // Prefer the passed country code from Twilio over CRM data.\n if ($countryCode === null && $crmCountryCode) {\n $countryCode = $crmCountryCode;\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n // Lookup who the message might be to/from based on their phone number.\n $lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $lookup,\n null,\n $user->getId()\n );\n }\n\n if ($opportunity === null) {\n $opportunities = $crmService->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $user->getId()\n );\n\n if (! empty($opportunities)) {\n $opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);\n }\n }\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // If their CRM has become disconnected, we really have no idea who the text customer is.\n $lead = $account = $opportunity = $contact = $stage = null;\n }\n\n if ($countryCode === null) {\n $countryCode = $user->country_code;\n }\n\n if ($type === Activity::TYPE_SMS_OUTBOUND) {\n $to = phone_e164($countryCode, $to);\n\n // Check they are allowed to text this number.\n if ($this->activityService->userCanText($user, $to, $countryCode) === false) {\n throw new \\InvalidArgumentException('Recipient in block list for this user.');\n }\n\n $title = 'Text to ' . phone_national($countryCode, $to);\n $status = Activity::STATUS_QUEUED;\n } else {\n $title = 'Text from ' . phone_national($countryCode, $from);\n $status = Activity::STATUS_RECEIVED;\n }\n\n $category = $this->getActivityType($user, $type);\n\n $activity = $user->activities()->create([\n 'provider' => Activity::PROVIDER_TWILIO,\n 'telephony_provider_id' => $messageId ?? null,\n 'type' => $type,\n 'title' => $title,\n 'description' => $message,\n 'crm_configuration_id' => $team->crm_id,\n 'playbook_category_id' => $category->id ?? null,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => $status,\n 'scheduled_start_time' => now(),\n ]);\n\n if (! $activity instanceof Activity) {\n throw new LogicException('Activity model expected');\n }\n\n // Create two participants to store the sender/receiver of the message.\n $userParticipant = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $user->softphone_number,\n ]);\n\n $prospectParticipant = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'name' => mb_strimwidth($activity->prospect_name, 0, 100),\n 'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,\n ]);\n\n // Refresh the model to fetch participants we just created so they'll be pushed to ES\n $activity->refresh();\n\n // Depending on the message direction, setup the from/to.\n $activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $userParticipant->id\n : $prospectParticipant->id;\n\n $activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND\n ? $prospectParticipant->id\n : $userParticipant->id;\n\n $activity->save();\n\n return $activity;\n }\n\n public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string\n {\n $team = $user->getTeam();\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n }\n\n // Query the CRM for full details of this customer.\n try {\n [, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return $user->getCountryCode();\n }\n\n // Prefer the passed country code from CRM data to the user.\n if ($countryCode === null) {\n $countryCode = $user->getCountryCode();\n }\n\n return $countryCode;\n }\n\n private function getActivityType(User $user, string $type): ?PlaybookCategory\n {\n $getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);\n\n return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);\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}]...
|
-5111928977143741995
|
-3204935344002590708
|
typing_pause
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
22
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Telephony;
use Jiminny\Component\Activity\Services\GetDefaultActivityTypeService;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Activity;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
class TextMessagingService
{
private ?Team $team = null;
private ?TwilioClient $twilioClient = null;
public function __construct(
private readonly ProviderRegistry $providerRegistry,
private readonly ActivityService $activityService,
private readonly Webhook $urlGenerator,
private readonly TwilioClientBuilder $twilioClientBuilder,
) {
}
/**
* @throws ConfigurationException
*/
public function setTeam(Team $team): void
{
$this->team = $team;
$this->twilioClient = $this->twilioClientBuilder->build($team);
}
/**
* @throws \Exception
*/
public function send(User $from, string $to, string $message): MessageInstance
{
if ($this->team->id !== $from->team_id) {
throw new \InvalidArgumentException('Sender does not belong to this team.');
}
$payload = [
'from' => $from->softphone_number,
'body' => $message,
'smartEncoded' => true, // Detect Unicode characters that have a similar GSM-7 character and replace.
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.sms.event'),
];
if ($this->team->twilio_messaging_sid !== null) {
$payload['messagingServiceSid'] = $this->team->twilio_messaging_sid;
} else {
$payload['applicationSid'] = $this->team->twilio_sms_sid;
}
return $this->twilioClient
->messages
->create($to, $payload);
}
/**
* @throws TwilioException
*/
public function redact(Activity $activity): void
{
$this->twilioClient
->messages($activity->telephony_provider_id)
->delete();
}
public function buildActivity(
?string $messageId,
string $type,
User $user,
string $from,
string $to,
string $message,
?string $customerId,
?string $objectType,
?string $countryCode
): Activity {
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
try {
if ($customerId) {
// Query the CRM for full details of this customer.
[$lead, $account, $opportunity, $contact, $stage, $crmCountryCode] = $crmService->parseRecords($customerId, $objectType);
// Prefer the passed country code from Twilio over CRM data.
if ($countryCode === null && $crmCountryCode) {
$countryCode = $crmCountryCode;
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
// Lookup who the message might be to/from based on their phone number.
$lookup = $type === Activity::TYPE_SMS_INBOUND ? $from : $to;
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$lookup,
null,
$user->getId()
);
}
if ($opportunity === null) {
$opportunities = $crmService->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$user->getId()
);
if (! empty($opportunities)) {
$opportunity = $crmService->syncOpportunity($opportunities[0]['crmId']);
}
}
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// If their CRM has become disconnected, we really have no idea who the text customer is.
$lead = $account = $opportunity = $contact = $stage = null;
}
if ($countryCode === null) {
$countryCode = $user->country_code;
}
if ($type === Activity::TYPE_SMS_OUTBOUND) {
$to = phone_e164($countryCode, $to);
// Check they are allowed to text this number.
if ($this->activityService->userCanText($user, $to, $countryCode) === false) {
throw new \InvalidArgumentException('Recipient in block list for this user.');
}
$title = 'Text to ' . phone_national($countryCode, $to);
$status = Activity::STATUS_QUEUED;
} else {
$title = 'Text from ' . phone_national($countryCode, $from);
$status = Activity::STATUS_RECEIVED;
}
$category = $this->getActivityType($user, $type);
$activity = $user->activities()->create([
'provider' => Activity::PROVIDER_TWILIO,
'telephony_provider_id' => $messageId ?? null,
'type' => $type,
'title' => $title,
'description' => $message,
'crm_configuration_id' => $team->crm_id,
'playbook_category_id' => $category->id ?? null,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => $status,
'scheduled_start_time' => now(),
]);
if (! $activity instanceof Activity) {
throw new LogicException('Activity model expected');
}
// Create two participants to store the sender/receiver of the message.
$userParticipant = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $user->softphone_number,
]);
$prospectParticipant = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'name' => mb_strimwidth($activity->prospect_name, 0, 100),
'phone_number' => $type === Activity::TYPE_SMS_OUTBOUND ? $to : $from,
]);
// Refresh the model to fetch participants we just created so they'll be pushed to ES
$activity->refresh();
// Depending on the message direction, setup the from/to.
$activity->from_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $userParticipant->id
: $prospectParticipant->id;
$activity->to_participant_id = $type === Activity::TYPE_SMS_OUTBOUND
? $prospectParticipant->id
: $userParticipant->id;
$activity->save();
return $activity;
}
public function convertCustomerToCountryCode(User $user, string $customerId, ?string $objectType): string
{
$team = $user->getTeam();
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
}
// Query the CRM for full details of this customer.
try {
[, , , , , $countryCode] = $crmService->parseRecords($customerId, $objectType);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return $user->getCountryCode();
}
// Prefer the passed country code from CRM data to the user.
if ($countryCode === null) {
$countryCode = $user->getCountryCode();
}
return $countryCode;
}
private function getActivityType(User $user, string $type): ?PlaybookCategory
{
$getDefaultActivityTypeService = app(GetDefaultActivityTypeService::class);
return $getDefaultActivityTypeService->getForUserAndChannel($user, $type);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
70976
|
NULL
|
NULL
|
NULL
|
|
70979
|
2550
|
8
|
2026-05-22T11:01:44.500705+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447704500_m1.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete mode100644 app/Component/ES/Processor/Traits/WorkerIdTrait.phpcreate mode 100644 app/Component/ES/QueuePriorityEnum.phpTrginusWorker/aTrai+: Phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, use itat your ownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLater+ED→Jiminny ...& 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# support# thank-yous# the_people_of jimi...^ Direct messagesP. Galya Dimitrova ". Nikolay YankovP. Aneliya Angelova. Stoyan Tanevd James Graham> 0(all100% С8 • Fri 22 May 14:01:44Describe what you are looking for# releases8 226 0• MessagesProject: appC FilesBookmarksM/kan.Today ~J26+10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubNewCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
NULL
|
-2269136524105539994
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV SlackFileEditViewGoHistoryWindowHelpAPP (DOCKERDEV (-zsh)O $82APP (-zsh)tests/Unit/Services/Activity/RingCentral/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/ServiceTest.phptests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.phptests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.phptests/Unit/Services/Mail/0ffice/EmailApiClientTest.phptests/Unit/Services/Mail/TextRelayServiceTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php79 files changed, 4254 insertions(+), 2020 deletions(-)create mode 100644 .github/claude-reviewer/no-ticket-warning.txtcreate mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjsdelete mode 100644 app/Component/ES/Processor/BatchStatusInterface.phpcreatemode 100644 app/Component/ES/Processor/RedisEntityExtractor.phpdelete mode100644 app/Component/ES/Processor/Traits/WorkerIdTrait.phpcreate mode 100644 app/Component/ES/QueuePriorityEnum.phpTrginusWorker/aTrai+: Phpcreate mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModacreate mode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Asdeletemode100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/Ascreatemode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.phpdelete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpcreate mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.phpcreate mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-imporSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phWhat's next:Try Docker Debug forseamless, persistent debugging tools in any container or image →Learn moreat [URL_WITH_CREDENTIALS] (JY-20963-fix-import-on-deleted-ent|docker exec -it docker_lamp_1./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.phPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHPruntime: 8.3.30Running analysis on 7 cores with 10 files perprocess.Parallel runner is an experimental feature and may be unstable, use itat your ownrisk.Loaded config default from".php-cs-fixer.dist.php"5691/5691100%Fixed 0 of 5691 files in 96.077 seconds, 67.00 MBmemory used•HomeDMsActivityFilesLater+ED→Jiminny ...& 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# support# thank-yous# the_people_of jimi...^ Direct messagesP. Galya Dimitrova ". Nikolay YankovP. Aneliya Angelova. Stoyan Tanevd James Graham> 0(all100% С8 • Fri 22 May 14:01:44Describe what you are looking for# releases8 226 0• MessagesProject: appC FilesBookmarksM/kan.Today ~J26+10:26:05Tag:View JobGitHub APP1:27 PM4 new commits pushed tomaster byTodorStamatovJiminnycbf9b72b - JY-20955: fix calendar eventduplication checkafe8dceg - Merge branch 'master' into JY-20955-multiple-calendars-events218a8602 - Merge branch 'master' into JY-20955-multiple-calendars-eventsbe560cbf - Merge pull request #12119 fromjiminny/JY-20955-multiple-calendars-eventsjiminny/app | Added by GitHubNewCircleCl APP 1:53 PMDeployment Successful!Project: appWhen:05/22/202610:53:15Tag:View JobMessage #releases+Aa..•...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
70980
|
2551
|
13
|
2026-05-22T11:01:44.607086+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-22/1779 /Users/lukas/.screenpipe/data/data/2026-05-22/1779447704607_m2.jpg...
|
PhpStorm
|
faVsco.js – TextMessagingService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormcodeFV faVsco.js°9 master kroledey© Insigh PhostormcodeFV faVsco.js°9 master kroledey© InsightsService.php© NumberAllocator.php1extmessagingservice.onpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.php5814>123 19)c Userservice.onrLocal changesShelfConsole xLog x +env.local aon@.liminnvDehuaCommandinhn ann/ConcolelCommandephp logging.php configg SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManag© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal appC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsservice.private readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotionG-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidpublic function delete(Request Srequest, string $uuid): JsonResponsel...}+ → @ Side-by-side viewer ~Do not ianore vHighlight words 13 g ?81aee7aadScontact = nulliCctane = null.Connontunitv = null.// Setup services.ScrmService = $this->crmProviderRegistry->get(Steam->crm->provider):if (! Suser->isCrmRequiredo){ScrmService->setUser(Suser->team->owner):} else {scrmservice->serusersuser=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilionlient = nultpublic function __construct(private readonly ProviderRegistry $providerRegistry,private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* Athrows \ExceptionPsvewi all nlaces in the code where we could uceoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancelcurrent versionScontact = null.$stage = null;Sopportunity = null;try {ScrmResolver = app(Crm0wnerResolver::class'team' => $team'integrationAdmin' => $usen'providerSlug' => $team->getCrmConfiqurationO->qetProviderNameOScrmService = ScrmResolver->prepareCrmService0:} catch (\Throwable Sthrowable){100% 2. Fri 22 May 14:01:44U AskJiminnyReportActivityServiceTest vCascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnckcalonFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .2 differencasW Windsurf Teamc1-1UTE.8io 4 spaces...
|
NULL
|
-6292218790865168083
|
NULL
|
click
|
ocr
|
NULL
|
PhostormcodeFV faVsco.js°9 master kroledey© Insigh PhostormcodeFV faVsco.js°9 master kroledey© InsightsService.php© NumberAllocator.php1extmessagingservice.onpc I willoclient.onpcIwlloclientbullder.pnpcIwilloservice.ono© TwilioSubAccountCreator.php•D UserPilot_ WebhookC AbstractService.php© ActivitvProviderFactory.php©) [EMAIL]© ConferenceService.phpC)InsiahtSeatService.oho© InstantMeetingService.php© IntercomService.php© IpapiClient.php(c) Inan Service nhnl© ParticipantShareService.php@ PlanhatService.php© PlaybackService.phpPlaybackVideoOnlyService.php© PlaybookCategoryService.php© PlaylistGeneratorinterface.phpkesolveleamermeonnection.ong© SimpleThrottleService.php@ ClackService.php© SocialAccountService.php© SoftPhoneService.php© TeamDeactivatedService.php© TeamOwnerService.php© TeamService.php© TranscodeParameterResolver.php5814>123 19)c Userservice.onrLocal changesShelfConsole xLog x +env.local aon@.liminnvDehuaCommandinhn ann/ConcolelCommandephp logging.php configg SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManag© TextMessagingService.php app/Services/TelephonyUnversioned Files 9 filesE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/R6 favicon.ico publicEids.txt aprTraw saL querv.sal appC) SimulateWebhooksCommand.oho app/Console/Commands/Crm/HubspotM+ WEBHOOK FILTERING IMPLEMENTATION.md appsynerorlannatpnp©PlaybackController.phppnp api.phpC) CreateSmsSentEvent.php(C) PlanhatActivityListener.pho(C)AskAnythingPromptService.php© AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api v2.phpC) RequestGenerateReportJob.pho(C) AutomatedReportResult.php© AutomatedReport.phpclass UserAutomatedReportsController extends ControllerBI4NLAY7а (61pubulc tunccion -_conscruceprivate readonly AutomatedReportsRepository $automatedReportsRepositoryprivace readonly Aucomareckeporcsservice saucomareakeportsservice.private readonly Apikesponseservice sapikesponseservice,private readonly kesponse sresponse.private readonly PlanhatService $planhatService,{...1PosTlaoi/vilautomated-reports/interest 1 usagepublic function trackInterest(Request Srequest): JsonResponsef...?* othrows ApplicationExceotionG-Taoi/vlautomated-renortsnublic function List(Request Srequest): IsonResnonsef.}DELETE lanilv1/automated-renorts/uuidpublic function delete(Request Srequest, string $uuid): JsonResponsel...}+ → @ Side-by-side viewer ~Do not ianore vHighlight words 13 g ?81aee7aadScontact = nulliCctane = null.Connontunitv = null.// Setup services.ScrmService = $this->crmProviderRegistry->get(Steam->crm->provider):if (! Suser->isCrmRequiredo){ScrmService->setUser(Suser->team->owner):} else {scrmservice->serusersuser=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextMessagingService.php X A console [EU]A console [STAGING]stohenamespace Jininny services lelephony>use ...class TextMessaqngServiceorivate ?Team steam = nulu3 usadesorivate ?TwilioClient Stwilionlient = nultpublic function __construct(private readonly ProviderRegistry $providerRegistry,private readonly ActivityService SactivityService,private readonly Webhook SurlGeneratorprivate readonly TwilioClientBuilder $twilioClientBuilder,* Othrows ConfigurationExceptionpublic function setTeam(Team $team): voidsth1s->team = steam:Sthis-›twilioClient = Sthis->twilioClientBuilder->build(Steam):* Athrows \ExceptionPsvewi all nlaces in the code where we could uceoublic function sendUser Sfrom. strina Sto. strina Smessage): Messagelnstancelcurrent versionScontact = null.$stage = null;Sopportunity = null;try {ScrmResolver = app(Crm0wnerResolver::class'team' => $team'integrationAdmin' => $usen'providerSlug' => $team->getCrmConfiqurationO->qetProviderNameOScrmService = ScrmResolver->prepareCrmService0:} catch (\Throwable Sthrowable){100% 2. Fri 22 May 14:01:44U AskJiminnyReportActivityServiceTest vCascadeInvestigate Foreign KeSalestorce Token Fallb+0 ..122 V1 V1 AVThe file is still off lot of warning just see what is wrongThoughtsInought tor 1sI see the issue - the try-catch structure is broken. The catch block on line 147 for SocialAccountTokenInvalidExceptionhas no matching try block because the CRM usage code (lines 115-146) is not wrapped in a try block.• TextMessagingService.phrnckcalonFixed the trv-catch olock structure in TextMessagingService, The issue was that the catch olock tor Social AccountTokeninvalidException (line 147) had no matching try block - the CRM usage code wasn't wrapped in a try block after I added the1. SoftPhoneManager.php - Added CrmOwnerResolver fallback for both outbound and inbound Twilio calls2. TextMessagingService.php - Added CrmOwnerResolver fallback for both buildActivity and convertCustomerToCouncrylode metnoas, with proper try-catch structureBoth imolementations now:• First try CrmOwnerResolver to find an active Salesforce token from the organization• If that fails, fall back to the original direct user assignment pattern• Let existing outer catch blocks handle any Socia AccountTokenInvalidException by settina CRM data to nullThis ensures Twilio calls will use another active Salesforce token when the admin token expires, preventing "UnknownGal .2 differencasW Windsurf Teamc1-1UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|