M3 Integration fixes
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
+1
-2
@@ -6,12 +6,12 @@ namespace Modules\Helpdesk\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
@@ -198,5 +198,4 @@ final class AttachmentDownloadController extends Controller
|
||||
default => 'text',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
+146
-100
@@ -15,6 +15,20 @@ use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
||||
|
||||
final class GitWebhookController
|
||||
{
|
||||
private const REJECTED = 'rejected';
|
||||
|
||||
private const SIGNATURE_FAILED = 'Git webhook signature or token validation failed.';
|
||||
|
||||
private const SHA256_PREFIX = 'sha256=';
|
||||
|
||||
private const SHA1_PREFIX = 'sha1=';
|
||||
|
||||
private const REQUEST_ID_HEADER = 'X-Request-Id';
|
||||
|
||||
private const GITHUB_DELIVERY_HEADER = 'X-GitHub-Delivery';
|
||||
|
||||
private const GITHUB_EVENT_HEADER = 'X-GitHub-Event';
|
||||
|
||||
/**
|
||||
* Ingest a signed Git provider webhook for a registered Helpdesk repository.
|
||||
*/
|
||||
@@ -24,34 +38,31 @@ final class GitWebhookController
|
||||
$event = $this->eventName($request, $provider);
|
||||
$deliveryId = $this->deliveryId($request, $provider);
|
||||
$rawPayload = $request->getContent();
|
||||
$status = 202;
|
||||
$payload = [];
|
||||
|
||||
if (! $repository->is_active) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Repository is inactive.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 403);
|
||||
$result = $this->rejectedResult('Repository is inactive.');
|
||||
$status = 403;
|
||||
} elseif (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) {
|
||||
$result = $this->rejectedResult(self::SIGNATURE_FAILED);
|
||||
$status = 403;
|
||||
} else {
|
||||
try {
|
||||
/** @var array<string, mixed> $payload */
|
||||
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
||||
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
||||
} catch (JsonException) {
|
||||
$result = $this->rejectedResult('Invalid JSON webhook payload.');
|
||||
$status = 400;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 403);
|
||||
if ($status !== 202) {
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result, $payload);
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var array<string, mixed> $payload */
|
||||
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Invalid JSON webhook payload.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 400);
|
||||
}
|
||||
|
||||
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
||||
|
||||
return response()->json($result, 202);
|
||||
return response()->json($result, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,42 +74,34 @@ final class GitWebhookController
|
||||
$event = $this->eventName($request, $provider);
|
||||
$deliveryId = $this->deliveryId($request, $provider);
|
||||
$rawPayload = $request->getContent();
|
||||
$status = 202;
|
||||
$repository = null;
|
||||
|
||||
try {
|
||||
/** @var array<string, mixed> $payload */
|
||||
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
||||
$repository = $this->repositoryForGiteaPayload($payload);
|
||||
|
||||
if (! $repository instanceof GitRepository) {
|
||||
$result = $this->rejectedResult('No active Helpdesk Git repository matched the Gitea webhook payload.');
|
||||
$status = 404;
|
||||
} elseif (! $this->hasValidGiteaSecurity($request, $repository, $provider, $rawPayload)) {
|
||||
$result = $this->rejectedResult(self::SIGNATURE_FAILED);
|
||||
$status = 403;
|
||||
} else {
|
||||
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
||||
}
|
||||
} catch (JsonException) {
|
||||
return response()->json([
|
||||
'status' => 'rejected',
|
||||
'message' => 'Invalid JSON webhook payload.',
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
], 400);
|
||||
$payload = [];
|
||||
$result = $this->rejectedResult('Invalid JSON webhook payload.');
|
||||
$status = 400;
|
||||
}
|
||||
|
||||
$repository = $this->repositoryForGiteaPayload($payload);
|
||||
|
||||
if (! $repository instanceof GitRepository) {
|
||||
return response()->json([
|
||||
'status' => 'rejected',
|
||||
'message' => 'No active Helpdesk Git repository matched the Gitea webhook payload.',
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (! $this->hasValidGiteaSecurity($request, $repository, $provider, $rawPayload)) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
if ($repository instanceof GitRepository && $status !== 202) {
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result, $payload);
|
||||
|
||||
return response()->json($result, 403);
|
||||
}
|
||||
|
||||
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
||||
|
||||
return response()->json($result, 202);
|
||||
return response()->json($result, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,8 +146,8 @@ final class GitWebhookController
|
||||
$sha256Signature = (string) $request->header('X-Hub-Signature-256', '');
|
||||
$sha1Signature = (string) $request->header('X-Hub-Signature', '');
|
||||
|
||||
return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $sha256Signature)
|
||||
|| hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $sha1Signature);
|
||||
return hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $sha256Signature)
|
||||
|| hash_equals(self::SHA1_PREFIX . hash_hmac('sha1', $rawPayload, $secret), $sha1Signature);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,8 +161,8 @@ final class GitWebhookController
|
||||
$sha1 = hash_hmac('sha1', $rawPayload, $secret);
|
||||
|
||||
return hash_equals($sha256, $giteaSignature)
|
||||
|| hash_equals('sha256=' . $sha256, $giteaSignature)
|
||||
|| hash_equals('sha1=' . $sha1, $giteaSignature)
|
||||
|| hash_equals(self::SHA256_PREFIX . $sha256, $giteaSignature)
|
||||
|| hash_equals(self::SHA1_PREFIX . $sha1, $giteaSignature)
|
||||
|| $this->hasValidGitHubSignature($request, $secret, $rawPayload)
|
||||
|| $this->hasValidGenericSignature($request, $secret, $rawPayload);
|
||||
}
|
||||
@@ -171,7 +174,7 @@ final class GitWebhookController
|
||||
{
|
||||
$signature = (string) $request->header('X-Hub-Signature', '');
|
||||
|
||||
return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
return hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals($secret, (string) $request->header('X-Helpdesk-Token', ''));
|
||||
}
|
||||
|
||||
@@ -184,8 +187,8 @@ final class GitWebhookController
|
||||
$signature = (string) ($request->header('X-Hub-Signature-256') ?: $request->header('X-Hub-Signature'));
|
||||
|
||||
return hash_equals($secret, $token)
|
||||
|| hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $signature);
|
||||
|| hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals(self::SHA1_PREFIX . hash_hmac('sha1', $rawPayload, $secret), $signature);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,63 +233,94 @@ final class GitWebhookController
|
||||
->where('is_active', true)
|
||||
->whereRaw('LOWER(provider) IN (?, ?)', ['gitea', 'git'])
|
||||
->get();
|
||||
$matchedRepository = null;
|
||||
|
||||
if ($repositories->isEmpty()) {
|
||||
Log::warning('Helpdesk Gitea webhook received but no active Gitea/git repository is configured.');
|
||||
} else {
|
||||
$normalisedPayloadValues = $this->normalisedPayloadRepositoryValues($payload);
|
||||
$matchedRepository = $repositories->first(
|
||||
fn (GitRepository $repository): bool => $this->repositoryMatchesPayload($repository, $normalisedPayloadValues),
|
||||
);
|
||||
|
||||
return null;
|
||||
if (! $matchedRepository instanceof GitRepository && $repositories->count() === 1) {
|
||||
$matchedRepository = $repositories->first();
|
||||
} elseif (! $matchedRepository instanceof GitRepository) {
|
||||
Log::warning('Helpdesk Gitea webhook did not match any configured repository.', [
|
||||
'payload_repository' => Arr::get($payload, 'repository.full_name') ?: Arr::get($payload, 'repository.clone_url') ?: Arr::get($payload, 'repository.html_url'),
|
||||
'configured_repository_count' => $repositories->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$payloadValues = array_values(array_filter([
|
||||
return $matchedRepository instanceof GitRepository ? $matchedRepository : null;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $payload
|
||||
* @return list<string>
|
||||
*/
|
||||
private function normalisedPayloadRepositoryValues(array $payload): array
|
||||
{
|
||||
$name = Arr::get($payload, 'repository.name');
|
||||
$owner = Arr::get($payload, 'repository.owner.username');
|
||||
|
||||
return $this->normaliseRepositoryValues([
|
||||
Arr::get($payload, 'repository.full_name'),
|
||||
Arr::get($payload, 'repository.name'),
|
||||
$name,
|
||||
Arr::get($payload, 'repository.html_url'),
|
||||
Arr::get($payload, 'repository.clone_url'),
|
||||
Arr::get($payload, 'repository.ssh_url'),
|
||||
Arr::get($payload, 'repository.url'),
|
||||
Arr::get($payload, 'repository.owner.username') && Arr::get($payload, 'repository.name')
|
||||
? Arr::get($payload, 'repository.owner.username') . '/' . Arr::get($payload, 'repository.name')
|
||||
: null,
|
||||
], static fn (mixed $value): bool => is_scalar($value) && trim((string) $value) !== ''));
|
||||
$owner && $name ? $owner . '/' . $name : null,
|
||||
]);
|
||||
}
|
||||
|
||||
$normalisedPayloadValues = array_values(array_filter(array_map(
|
||||
fn (mixed $value): ?string => $this->normaliseRepositoryIdentity((string) $value),
|
||||
$payloadValues,
|
||||
)));
|
||||
/** @param list<string> $payloadValues */
|
||||
private function repositoryMatchesPayload(GitRepository $repository, array $payloadValues): bool
|
||||
{
|
||||
$repositoryValues = $this->normaliseRepositoryValues([
|
||||
$repository->remote_url,
|
||||
$repository->name,
|
||||
Str::of((string) $repository->remote_url)->afterLast(':')->beforeLast('.git')->value(),
|
||||
]);
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$repositoryValues = array_filter([
|
||||
$repository->remote_url,
|
||||
$repository->name,
|
||||
Str::of((string) $repository->remote_url)->afterLast(':')->beforeLast('.git')->value(),
|
||||
], static fn (mixed $value): bool => is_scalar($value) && trim((string) $value) !== '');
|
||||
|
||||
$normalisedRepositoryValues = array_values(array_filter(array_map(
|
||||
fn (mixed $value): ?string => $this->normaliseRepositoryIdentity((string) $value),
|
||||
$repositoryValues,
|
||||
)));
|
||||
|
||||
foreach ($normalisedPayloadValues as $payloadValue) {
|
||||
foreach ($normalisedRepositoryValues as $repositoryValue) {
|
||||
if ($payloadValue === $repositoryValue
|
||||
|| str_ends_with($repositoryValue, '/' . $payloadValue)
|
||||
|| str_ends_with($payloadValue, '/' . $repositoryValue)) {
|
||||
return $repository;
|
||||
}
|
||||
foreach ($payloadValues as $payloadValue) {
|
||||
foreach ($repositoryValues as $repositoryValue) {
|
||||
if ($this->repositoryIdentitiesMatch($payloadValue, $repositoryValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($repositories->count() === 1) {
|
||||
return $repositories->first();
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param array<int, mixed> $values
|
||||
* @return list<string>
|
||||
*/
|
||||
private function normaliseRepositoryValues(array $values): array
|
||||
{
|
||||
$normalised = [];
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (! is_scalar($value) || trim((string) $value) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$identity = $this->normaliseRepositoryIdentity((string) $value);
|
||||
if ($identity !== null) {
|
||||
$normalised[] = $identity;
|
||||
}
|
||||
}
|
||||
|
||||
Log::warning('Helpdesk Gitea webhook did not match any configured repository.', [
|
||||
'payload_repository' => Arr::get($payload, 'repository.full_name') ?: Arr::get($payload, 'repository.clone_url') ?: Arr::get($payload, 'repository.html_url'),
|
||||
'configured_repository_count' => $repositories->count(),
|
||||
]);
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
return null;
|
||||
private function repositoryIdentitiesMatch(string $payloadValue, string $repositoryValue): bool
|
||||
{
|
||||
return $payloadValue === $repositoryValue
|
||||
|| str_ends_with($repositoryValue, '/' . $payloadValue)
|
||||
|| str_ends_with($payloadValue, '/' . $repositoryValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,17 +359,29 @@ final class GitWebhookController
|
||||
return implode('/', array_slice($segments, -2));
|
||||
}
|
||||
|
||||
/** @return array{status: string, message: string, commit_count: int, linked_count: int, ignored_count: int} */
|
||||
private function rejectedResult(string $message): array
|
||||
{
|
||||
return [
|
||||
'status' => self::REJECTED,
|
||||
'message' => $message,
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the event name from provider-specific headers.
|
||||
*/
|
||||
private function eventName(Request $request, string $provider): ?string
|
||||
{
|
||||
return match ($provider) {
|
||||
'github' => $request->header('X-GitHub-Event'),
|
||||
'github' => $request->header(self::GITHUB_EVENT_HEADER),
|
||||
'gitlab' => $request->header('X-Gitlab-Event'),
|
||||
'gitea' => $request->header('X-Gitea-Event') ?: $request->header('X-Gitea-Event-Type') ?: $request->header('X-GitHub-Event'),
|
||||
'gitea' => $request->header('X-Gitea-Event') ?: $request->header('X-Gitea-Event-Type') ?: $request->header(self::GITHUB_EVENT_HEADER),
|
||||
'bitbucket' => $request->header('X-Event-Key'),
|
||||
default => $request->header('X-Helpdesk-Event') ?: $request->header('X-GitHub-Event') ?: $request->header('X-Gitlab-Event') ?: 'push',
|
||||
default => $request->header('X-Helpdesk-Event') ?: $request->header(self::GITHUB_EVENT_HEADER) ?: $request->header('X-Gitlab-Event') ?: 'push',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -345,11 +391,11 @@ final class GitWebhookController
|
||||
private function deliveryId(Request $request, string $provider): ?string
|
||||
{
|
||||
return match ($provider) {
|
||||
'github' => $request->header('X-GitHub-Delivery'),
|
||||
'gitlab' => $request->header('X-Gitlab-Event-UUID') ?: $request->header('X-Request-Id'),
|
||||
'gitea' => $request->header('X-Gitea-Delivery') ?: $request->header('X-GitHub-Delivery') ?: $request->header('X-Request-Id'),
|
||||
'bitbucket' => $request->header('X-Request-UUID') ?: $request->header('X-Request-Id'),
|
||||
default => $request->header('X-Helpdesk-Delivery') ?: $request->header('X-GitHub-Delivery') ?: $request->header('X-Request-Id'),
|
||||
'github' => $request->header(self::GITHUB_DELIVERY_HEADER),
|
||||
'gitlab' => $request->header('X-Gitlab-Event-UUID') ?: $request->header(self::REQUEST_ID_HEADER),
|
||||
'gitea' => $request->header('X-Gitea-Delivery') ?: $request->header(self::GITHUB_DELIVERY_HEADER) ?: $request->header(self::REQUEST_ID_HEADER),
|
||||
'bitbucket' => $request->header('X-Request-UUID') ?: $request->header(self::REQUEST_ID_HEADER),
|
||||
default => $request->header('X-Helpdesk-Delivery') ?: $request->header(self::GITHUB_DELIVERY_HEADER) ?: $request->header(self::REQUEST_ID_HEADER),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
+1
-1
@@ -45,6 +45,7 @@ final class TicketAttachmentStoreController extends Controller
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
@@ -71,5 +72,4 @@ final class TicketAttachmentStoreController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
+2
-1
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Helpdesk\Exceptions\TicketPersistenceException;
|
||||
use Modules\Helpdesk\Models\KanbanBoard;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
@@ -68,7 +69,7 @@ final class TicketBoardController extends Controller
|
||||
->update($updates);
|
||||
|
||||
if ($affected < 1) {
|
||||
throw new \RuntimeException('Ticket board update did not persist.');
|
||||
throw new TicketPersistenceException('Ticket board update did not persist.');
|
||||
}
|
||||
|
||||
return Ticket::query()
|
||||
|
||||
Regular → Executable
+42
-41
@@ -25,51 +25,52 @@ final class TicketCommentDeleteController extends Controller
|
||||
abort_unless((int) $comment->ticket_id === (int) $ticket->id, 404);
|
||||
|
||||
DB::transaction(function () use ($comment, $ticket, $user): void {
|
||||
$updates = [];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'tombstoned_at')) {
|
||||
$updates['tombstoned_at'] = now();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'deleted_by')) {
|
||||
$updates['deleted_by'] = (int) $user->id;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'delete_reason')) {
|
||||
$updates['delete_reason'] = 'Deleted by MPBA platform administrator.';
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_sequence')) {
|
||||
$updates['sync_sequence'] = ((int) ($comment->sync_sequence ?? 0)) + 1;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_hash')) {
|
||||
$updates['sync_hash'] = hash('sha256', implode('|', [
|
||||
(string) ($comment->guid ?? $comment->id),
|
||||
'deleted',
|
||||
now()->toISOString(),
|
||||
]));
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
if ($updates !== []) {
|
||||
DB::table('helpdesk_comments')
|
||||
->where('id', $comment->id)
|
||||
->update($updates);
|
||||
} else {
|
||||
$comment->delete();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
}
|
||||
$this->deleteComment($comment, (int) $user->id);
|
||||
$this->touchTicket($ticket);
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-comment-deleted', 'Response deleted.');
|
||||
}
|
||||
|
||||
private function deleteComment(Comment $comment, int $userId): void
|
||||
{
|
||||
$updates = $this->tombstoneUpdates($comment, $userId);
|
||||
|
||||
if ($updates === []) {
|
||||
$comment->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_comments')->where('id', $comment->id)->update($updates);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function tombstoneUpdates(Comment $comment, int $userId): array
|
||||
{
|
||||
$now = now();
|
||||
$candidates = [
|
||||
'tombstoned_at' => $now,
|
||||
'deleted_by' => $userId,
|
||||
'delete_reason' => 'Deleted by MPBA platform administrator.',
|
||||
'sync_sequence' => ((int) ($comment->sync_sequence ?? 0)) + 1,
|
||||
'sync_hash' => hash('sha256', implode('|', [(string) ($comment->guid ?? $comment->id), 'deleted', $now->toISOString()])),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
return array_filter(
|
||||
$candidates,
|
||||
static fn (mixed $value, string $column): bool => Schema::hasColumn('helpdesk_comments', $column),
|
||||
ARRAY_FILTER_USE_BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
private function touchTicket(Ticket $ticket): void
|
||||
{
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+1
-1
@@ -170,6 +170,7 @@ final class TicketCommentStoreController extends Controller
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
@@ -196,5 +197,4 @@ final class TicketCommentStoreController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
+1
-1
@@ -63,6 +63,7 @@ final class TicketStoreController extends Controller
|
||||
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
|
||||
*
|
||||
* @param bool $required Whether at least one uploaded file is required.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentFileRules(bool $required = false): array
|
||||
@@ -89,5 +90,4 @@ final class TicketStoreController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
+61
-52
@@ -35,62 +35,71 @@ final class TicketTimeLogController extends Controller
|
||||
$loggedAt = now();
|
||||
|
||||
DB::transaction(function () use ($ticket, $request, $minutes, $description, $loggedAt): void {
|
||||
if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) {
|
||||
$ticket->increment('time_spent_minutes', $minutes, [
|
||||
'last_activity_at' => $loggedAt,
|
||||
]);
|
||||
} elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => $loggedAt])->save();
|
||||
}
|
||||
|
||||
if (Schema::hasTable('helpdesk_time_logs')) {
|
||||
DB::table('helpdesk_time_logs')->insert([
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'logged_at' => $loggedAt,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
if (Schema::hasTable('helpdesk_ticket_time_logs')) {
|
||||
$payload = [
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'started_at')) {
|
||||
$payload['started_at'] = $loggedAt->copy()->subMinutes($minutes);
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'ended_at')) {
|
||||
$payload['ended_at'] = $loggedAt;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'logged_at')) {
|
||||
$payload['logged_at'] = $loggedAt;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'duration_seconds')) {
|
||||
$payload['duration_seconds'] = $minutes * 60;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'seconds')) {
|
||||
$payload['seconds'] = $minutes * 60;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_ticket_time_logs')->insert($payload);
|
||||
}
|
||||
$this->updateTicketActivity($ticket, $minutes, $loggedAt);
|
||||
$this->storeLegacyTimeLog($ticket, $request, $minutes, $description, $loggedAt);
|
||||
$this->storeTicketTimeLog($ticket, $request, $minutes, $description, $loggedAt);
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-action-updated', 'Time logged successfully.');
|
||||
}
|
||||
|
||||
private function updateTicketActivity(Ticket $ticket, int $minutes, $loggedAt): void
|
||||
{
|
||||
if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) {
|
||||
$ticket->increment('time_spent_minutes', $minutes, ['last_activity_at' => $loggedAt]);
|
||||
} elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => $loggedAt])->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function storeLegacyTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void
|
||||
{
|
||||
if (! Schema::hasTable('helpdesk_time_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_time_logs')->insert([
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'logged_at' => $loggedAt,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
private function storeTicketTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void
|
||||
{
|
||||
if (! Schema::hasTable('helpdesk_ticket_time_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
];
|
||||
|
||||
$optional = [
|
||||
'started_at' => $loggedAt->copy()->subMinutes($minutes),
|
||||
'ended_at' => $loggedAt,
|
||||
'logged_at' => $loggedAt,
|
||||
'duration_seconds' => $minutes * 60,
|
||||
'seconds' => $minutes * 60,
|
||||
];
|
||||
|
||||
foreach ($optional as $column => $value) {
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', $column)) {
|
||||
$payload[$column] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('helpdesk_ticket_time_logs')->insert($payload);
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user