feat: complete equipment handover workflow

This commit is contained in:
2026-08-04 17:15:51 +01:00
parent 809eab9d4a
commit d829f34580
24 changed files with 1800 additions and 28 deletions

View File

@@ -9,6 +9,7 @@ use Generator;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Throwable;
@@ -20,6 +21,11 @@ final class SnipeItClient
return filled(config('services.snipe_it.url')) && filled(config('services.snipe_it.read_token'));
}
public function isOperationallyConfigured(): bool
{
return filled(config('services.snipe_it.url')) && filled(config('services.snipe_it.operational_token'));
}
public function configuredUrl(): ?string
{
$url = trim((string) config('services.snipe_it.url'));
@@ -32,7 +38,7 @@ final class SnipeItClient
$pageSize = $limit ?? (int) config('services.snipe_it.page_size', 100);
$pageSize = max(1, min(500, $pageSize));
$response = $this->request()->get('/hardware', [
$response = $this->readRequest()->get('/hardware', [
'limit' => $pageSize,
'offset' => max(0, $offset),
'sort' => 'id',
@@ -92,7 +98,61 @@ final class SnipeItClient
} while ($received > 0 && $offset < $page->total);
}
private function request(): PendingRequest
/** @return array<string, mixed> */
public function checkoutHardware(
int $hardwareId,
int $statusId,
int $assignedUserId,
string $expectedCheckin,
?string $note = null,
): array {
$this->assertPositiveIdentifiers($hardwareId, $statusId, $assignedUserId);
$payload = [
'status_id' => $statusId,
'checkout_to_type' => 'user',
'assigned_user' => $assignedUserId,
'expected_checkin' => $expectedCheckin,
];
if (filled($note)) {
$payload['note'] = $note;
}
$response = $this->operationalRequest()->post("/hardware/{$hardwareId}/checkout", $payload);
return $this->operationPayload($response, 'entrega');
}
/** @return array<string, mixed> */
public function checkinHardware(
int $hardwareId,
int $statusId,
?int $locationId = null,
?string $note = null,
): array {
$this->assertPositiveIdentifiers($hardwareId, $statusId);
if ($locationId !== null && $locationId < 1) {
throw new SnipeItConfigurationException('O identificador da localização do Snipe-IT tem de ser positivo.');
}
$payload = ['status_id' => $statusId];
if ($locationId !== null) {
$payload['location_id'] = $locationId;
}
if (filled($note)) {
$payload['note'] = $note;
}
$response = $this->operationalRequest()->post("/hardware/{$hardwareId}/checkin", $payload);
return $this->operationPayload($response, 'devolução');
}
private function readRequest(): PendingRequest
{
if (! $this->isConfigured()) {
throw new SnipeItConfigurationException('A URL e o token de leitura do Snipe-IT ainda não estão configurados.');
@@ -116,6 +176,54 @@ final class SnipeItClient
);
}
private function operationalRequest(): PendingRequest
{
if (! $this->isOperationallyConfigured()) {
throw new SnipeItConfigurationException('A URL e o token operacional do Snipe-IT ainda não estão configurados.');
}
return Http::baseUrl($this->apiBaseUrl())
->acceptJson()
->asJson()
->withToken((string) config('services.snipe_it.operational_token'))
->withUserAgent(Str::slug((string) config('app.name')).'/inventory-operations')
->connectTimeout(min(5, max(1, (int) config('services.snipe_it.timeout', 10))))
->timeout(max(1, (int) config('services.snipe_it.timeout', 10)));
}
/** @return array<string, mixed> */
private function operationPayload(Response $response, string $operation): array
{
if (! $response->successful()) {
throw new SnipeItApiException(
"O Snipe-IT respondeu com HTTP {$response->status()} durante a {$operation}.",
$response->status(),
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new SnipeItApiException("O Snipe-IT devolveu uma resposta inválida durante a {$operation}.");
}
if (($payload['status'] ?? null) !== 'success') {
throw new SnipeItApiException("O Snipe-IT recusou a {$operation} apesar de responder com HTTP 200.");
}
/** @var array<string, mixed> $payload */
return $payload;
}
private function assertPositiveIdentifiers(int ...$identifiers): void
{
foreach ($identifiers as $identifier) {
if ($identifier < 1) {
throw new SnipeItConfigurationException('Os identificadores do Snipe-IT têm de ser positivos.');
}
}
}
private function apiBaseUrl(): string
{
$baseUrl = $this->configuredUrl();