Skip to content
9 changes: 9 additions & 0 deletions app/Exceptions/Integration/ExternalServiceException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ public static function unavailable(string $service, \Throwable $previous): self
previous: $previous,
);
}

public static function fromFailedResponse(string $service, string $message, array $context = []): self
{
return new self(
$message,
httpStatus: 503,
context: ['service' => $service] + $context,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 2 additions & 1 deletion app/Http/Controllers/InstallmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers;

use App\Exceptions\AppException;
use App\Models\Project;
use App\Services\InstallmentImportService;
use App\Services\InstallmentService;
Expand Down Expand Up @@ -37,7 +38,7 @@ public function import(
}

return back()->with('success', $message);
} catch (\InvalidArgumentException $e) {
} catch (AppException $e) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
report($e);

return back()->with('error', $e->getMessage());
Expand Down
57 changes: 20 additions & 37 deletions app/Http/Controllers/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,12 @@ public function assignProjectSupervisor(Request $request, ProjectSupervisorServi
'selected_supervisors.*' => 'exists:users,id',
]);

try {
$service->assign(
$data['selected_projects'],
$data['selected_supervisors']
);

return back()->with('success', 'Fiscais atribuídos com sucesso!');
} catch (\Throwable $e) {
report($e);

return back()->withErrors([
'message' => 'Erro ao atribuir fiscais. Tente novamente.',
]);
}
$service->assign(
$data['selected_projects'],
$data['selected_supervisors']
);

return back()->with('success', 'Fiscais atribuídos com sucesso!');
}

public function createDocument(Request $request, ProjectDocumentService $service)
Expand All @@ -202,29 +194,20 @@ public function createDocument(Request $request, ProjectDocumentService $service
'footer_layout' => 'nullable|in:none,three,full',
]);

try {

$service->createDocument(
selectedProjects: $data['selected_projects'],
content: $data['content'],
headerImages: $data['header_images'] ?? [],
footerImages: $data['footer_images'] ?? [],
type: DocumentType::from($data['type']),
headerLayout: $data['header_layout'] ?? 'none',
footerLayout: $data['footer_layout'] ?? 'none',
);

return back()->with(
'success',
'Documento criado com sucesso! Você pode editá-lo ou baixá-lo na seção de documentos do projeto.'
);
} catch (\Throwable $e) {
report($e);

return back()->withErrors([
'message' => $e->getMessage() ?: 'Erro ao criar documento. Tente novamente.',
]);
}
$service->createDocument(
selectedProjects: $data['selected_projects'],
content: $data['content'],
headerImages: $data['header_images'] ?? [],
footerImages: $data['footer_images'] ?? [],
type: DocumentType::from($data['type']),
headerLayout: $data['header_layout'] ?? 'none',
footerLayout: $data['footer_layout'] ?? 'none',
);

return back()->with(
'success',
'Documento criado com sucesso! Você pode editá-lo ou baixá-lo na seção de documentos do projeto.'
);
}

private function userCanActOnStage(?ProjectStage $currentStage): bool
Expand Down
51 changes: 11 additions & 40 deletions app/Http/Controllers/ProjectStageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
namespace App\Http\Controllers;

use App\Enums\Role;
use App\Exceptions\AppException;
use App\Http\Requests\Stages\ReturnStageRequest;
use App\Models\Project;
use App\Models\ProjectStage;
use App\Services\NotificationService;
use App\Services\ProjectStageService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class ProjectStageController extends Controller
{
Expand All @@ -24,46 +23,20 @@ public function advance(
Project $project,
ProjectStage $stage
) {
try {
$stage->load('project');

$nextStage = $this->stageService->advance($stage, $request->user());

$this->notificationService->notifyStageAdvanced($stage, $nextStage, $request->user());
$stage->load('project');

return back()->with('success', 'Processo tramitado com sucesso!');
} catch (ValidationException $e) {
throw $e;
} catch (AuthorizationException $e) {
\Sentry\captureException($e);
$nextStage = $this->stageService->advance($stage, $request->user());

return back()->withErrors(['message' => $e->getMessage()]);
} catch (\InvalidArgumentException $e) {
report($e);
$this->notificationService->notifyStageAdvanced($stage, $nextStage, $request->user());

return back()->withErrors(['message' => $e->getMessage()]);
} catch (\Throwable $e) {
report($e);

return back()->withErrors([
'message' => 'Erro ao tramitar processo: '.$e->getMessage(),
]);
}
return back()->with('success', 'Processo tramitado com sucesso!');
}

public function requestNextInstallment(Request $request, Project $project)
{
try {
$this->stageService->requestNextInstallment($project, $request->user());

return back();
} catch (AuthorizationException $e) {
return back()->withErrors(['message' => $e->getMessage()]);
} catch (\InvalidArgumentException $e) {
report($e);
$this->stageService->requestNextInstallment($project, $request->user());

return back()->withErrors(['message' => $e->getMessage()]);
}
return back();
}

public function return(
Expand All @@ -86,12 +59,10 @@ public function return(
);

return back()->with('success', 'O processo foi devolvido aos responsáveis!');
} catch (AuthorizationException $e) {
\Sentry\captureException($e);

return back()->with('error', $e->getMessage());
} catch (\InvalidArgumentException $e) {
report($e);
} catch (AppException $e) {
if ($e->shouldReport()) {
report($e);
}

return back()->with('error', $e->getMessage());
}
Expand Down
3 changes: 2 additions & 1 deletion app/Services/Documents/DocumentTypeRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Enums\DocumentPhase;
use App\Enums\DocumentType;
use App\Exceptions\Domain\BusinessRuleException;

class DocumentTypeRegistry
{
Expand Down Expand Up @@ -45,7 +46,7 @@ public function resolve(DocumentType $type, DocumentPhase $phase): array
$key = "{$type->value}+{$phase->value}";

if (! isset($this->map[$key])) {
throw new \InvalidArgumentException(
throw new BusinessRuleException(
"Combinação de tipo e fase inválida: tipo={$type->value}, fase={$phase->value}."
);
}
Expand Down
11 changes: 7 additions & 4 deletions app/Services/GoogleSheetsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace App\Services;

use App\Exceptions\Integration\ExternalServiceException;
use App\Models\Budget;
use App\Models\Formalization;
use App\Models\Project;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
Expand All @@ -23,6 +25,7 @@ public function __construct(
*
* @return array{columns: string[], rows: array<int, array<string, mixed>>}
*
* @throws ExternalServiceException
* @throws RuntimeException
*/
public function fetchSheet(string $spreadsheetId, string $sheetName): array
Expand All @@ -34,8 +37,8 @@ public function fetchSheet(string $spreadsheetId, string $sheetName): array
->get($url, ['tqx' => 'out:json', 'sheet' => $sheetName])
->throw()
->body();
} catch (RequestException $e) {
throw new RuntimeException("Failed to fetch Google Sheet: {$e->getMessage()}", previous: $e);
} catch (ConnectionException|RequestException $e) {
throw ExternalServiceException::unavailable('Google Sheets', $e);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
$json = $this->stripSecurityPrefix($raw);

Expand Down Expand Up @@ -214,8 +217,8 @@ private function fetchSheetWithLookup(string $spreadsheetId, string $sheetName):
->get($url, ['tqx' => 'out:json', 'sheet' => $sheetName])
->throw()
->body();
} catch (RequestException $e) {
throw new RuntimeException("Failed to fetch Google Sheet: {$e->getMessage()}", previous: $e);
} catch (ConnectionException|RequestException $e) {
throw ExternalServiceException::unavailable('Google Sheets', $e);
}

$table = $this->decodeTable($this->stripSecurityPrefix($raw));
Expand Down
5 changes: 3 additions & 2 deletions app/Services/InstallmentImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Services;

use App\Exceptions\Domain\BusinessRuleException;
use App\Models\Budget;
use App\Models\Installment;
use App\Models\Project;
Expand Down Expand Up @@ -32,7 +33,7 @@ public function import(
);

if ($data->isEmpty()) {
throw new \InvalidArgumentException(
throw new BusinessRuleException(
'A planilha enviada está vazia.'
);
}
Expand All @@ -50,7 +51,7 @@ public function import(
$projectsByNup = $this->buildProjectsByNup($projects);

if ($projectsByNup->isEmpty()) {
throw new \InvalidArgumentException(
throw new BusinessRuleException(
'Nenhum projeto possui processo vinculado para comparação com a planilha.'
);
}
Expand Down
46 changes: 31 additions & 15 deletions app/Services/MapasClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

namespace App\Services;

use App\Exceptions\Integration\ExternalServiceException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;

class MapasClient
{
private const SERVICE_NAME = 'Mapas Cultural';

private const AGENT_SELECT_FIELDS = [
'id',
'name',
Expand Down Expand Up @@ -120,18 +122,21 @@ private function get(
$response = $this->request($authenticated)->get($path, $query);

if ($response->failed()) {
throw new RuntimeException(sprintf(
'Erro na API Mapas [%s] %s: %s',
$response->status(),
$path,
(string) str($response->body())->limit(500)
));
throw ExternalServiceException::fromFailedResponse(
self::SERVICE_NAME,
sprintf('Erro na API Mapas [%s] %s', $response->status(), $path),
['path' => $path],
);
}

$json = $response->json();

if (! is_array($json)) {
throw new RuntimeException("Resposta inválida da API Mapas: {$path}");
throw ExternalServiceException::fromFailedResponse(
self::SERVICE_NAME,
"Resposta inválida da API Mapas: {$path}",
['path' => $path],
);
}

return $json;
Expand Down Expand Up @@ -194,11 +199,15 @@ public function downloadFileTo(string $url, string $absolutePath): array
if ($response->failed()) {
@unlink($absolutePath);

throw new RuntimeException(sprintf(
'Erro ao baixar arquivo do Mapas [%s]: %s',
$response->status(),
$url
));
throw ExternalServiceException::fromFailedResponse(
self::SERVICE_NAME,
sprintf(
'Erro ao baixar arquivo do Mapas [%s]: %s',
$response->status(),
$this->redactUrl($url)
),
['url' => $this->redactUrl($url)],
);
}

$mimeType = $this->resolveDownloadedMimeType(
Expand All @@ -209,8 +218,10 @@ public function downloadFileTo(string $url, string $absolutePath): array
if ($mimeType === 'text/html') {
@unlink($absolutePath);

throw new RuntimeException(
"Download inválido: o Mapas retornou HTML em vez de arquivo. URL: {$url}"
throw ExternalServiceException::fromFailedResponse(
self::SERVICE_NAME,
"Download inválido: o Mapas retornou HTML em vez de arquivo. URL: {$this->redactUrl($url)}",
['url' => $this->redactUrl($url)],
);
}

Expand All @@ -220,6 +231,11 @@ public function downloadFileTo(string $url, string $absolutePath): array
];
}

private function redactUrl(string $url): string
{
return (string) str($url)->before('?');
}

private function resolveDownloadedMimeType(?string $contentType, string $absolutePath): string
{
$mimeType = $contentType
Expand Down
4 changes: 2 additions & 2 deletions app/Services/NoticeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

namespace App\Services;

use App\Exceptions\Domain\BusinessRuleException;
use App\Models\Notice;
use App\Models\Project;
use Illuminate\Support\Collection;
use InvalidArgumentException;

class NoticeService
{
Expand Down Expand Up @@ -71,7 +71,7 @@ public function createFromMapasIfMissing(array $notice): Notice
$externalId = data_get($notice, 'id');

if (! $externalId) {
throw new InvalidArgumentException('Edital sem id externo.');
throw new BusinessRuleException('Edital sem id externo.');
}

return Notice::query()->firstOrCreate(
Expand Down
Loading
Loading