M3 Integration fixes

This commit is contained in:
Mark Gregory
2026-09-21 10:14:57 +01:00
parent 4af997c465
commit 6d60cbff84
5853 changed files with 79032 additions and 1825 deletions
+97 -68
View File
@@ -10,6 +10,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Modules\Helpdesk\Exceptions\TicketPersistenceException;
use Modules\Helpdesk\Models\Comment;
use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Status;
@@ -45,7 +46,7 @@ final class TicketService
}
}
throw new \RuntimeException('Unable to create a unique Helpdesk ticket reference.');
throw new TicketPersistenceException('Unable to create a unique Helpdesk ticket reference.');
}
/**
@@ -56,32 +57,8 @@ final class TicketService
*/
private function createInsideTransaction(User $user, array $data, array $files = []): Ticket
{
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
: null;
if (! $status) {
$status = Status::query()->where('slug', $defaultStatusSlug)->first();
}
if (! $status) {
$status = Status::query()->create([
'name' => 'New',
'slug' => $defaultStatusSlug,
'color' => 'sky',
'sort_order' => 1,
'is_closed' => false,
]);
}
$boardId = $data['board_id'] ?? null;
if (blank($boardId) && Schema::hasTable('helpdesk_kanban_boards')) {
$boardId = KanbanBoard::query()
->where('is_default', true)
->orderBy('sort_order')
->value('id') ?: KanbanBoard::query()->orderBy('sort_order')->value('id');
}
$status = $this->resolveInitialStatus();
$boardId = $this->resolveBoardId($data['board_id'] ?? null);
/** @var Ticket $ticket */
$ticket = Ticket::query()->create([
@@ -102,9 +79,7 @@ final class TicketService
'last_activity_at' => now(),
]);
if (! empty($data['tag_ids']) && is_array($data['tag_ids'])) {
$ticket->tags()->sync(array_values(array_unique(array_map('intval', $data['tag_ids']))));
}
$this->syncTags($ticket, $data['tag_ids'] ?? null);
$ticket = $this->automation->apply($ticket, $data);
$ticket = $this->sla->applyPolicy($ticket);
@@ -117,6 +92,43 @@ final class TicketService
return $ticket;
}
private function resolveInitialStatus(): Status
{
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
: null;
$status ??= Status::query()->where('slug', $defaultStatusSlug)->first();
return $status ?? Status::query()->create([
'name' => 'New',
'slug' => $defaultStatusSlug,
'color' => 'sky',
'sort_order' => 1,
'is_closed' => false,
]);
}
private function resolveBoardId(mixed $boardId): mixed
{
if (! blank($boardId) || ! Schema::hasTable('helpdesk_kanban_boards')) {
return $boardId;
}
return KanbanBoard::query()->where('is_default', true)->orderBy('sort_order')->value('id')
?: KanbanBoard::query()->orderBy('sort_order')->value('id');
}
private function syncTags(Ticket $ticket, mixed $tagIds): void
{
if (! is_array($tagIds) || $tagIds === []) {
return;
}
$ticket->tags()->sync(array_values(array_unique(array_map('intval', $tagIds))));
}
/**
* Determine whether a database error was caused by the ticket reference unique index.
*/
@@ -137,52 +149,69 @@ final class TicketService
public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null, ?string $subject = null): Comment
{
return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId, $subject): Comment {
$parent = $parentId === null
? null
: Comment::query()
->where('ticket_id', $ticket->id)
->findOrFail($parentId);
$guid = (string) Str::uuid();
$unid = strtoupper(hash('sha256', $guid . '|' . $ticket->id . '|' . microtime(true)));
$parentUnid = $parent?->unid;
$rootUnid = $parent?->root_unid ?: $parentUnid ?: $unid;
$plainTextBody = trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: '');
$payload = [
'parent_id' => $parentId,
'user_id' => $user->id,
'body' => $body,
'body_text' => $plainTextBody,
'is_internal' => $internal,
'guid' => $guid,
'unid' => $unid,
'parent_unid' => $parentUnid,
'root_unid' => $rootUnid,
'originator_id' => $unid . ':1',
'sync_sequence' => 1,
'sync_hash' => hash('sha256', $body),
'source_system' => 'wms-helpdesk',
];
if (Schema::hasColumn('helpdesk_comments', 'subject')) {
$payload['subject'] = trim((string) ($subject ?: ($parentId === null ? 'Response' : 'RE: Response')));
}
$parent = $this->resolveParentComment($ticket, $parentId);
$payload = $this->commentPayload($ticket, $user, $body, $internal, $parent, $parentId, $subject);
/** @var Comment $comment */
$comment = $ticket->comments()->create($payload);
$ticket->forceFill(['last_activity_at' => now()])->save();
if (! $internal) {
$this->sla->recordPublicResponse($ticket, $user);
}
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
$this->workflow->dispatch($ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket, $parentId === null ? 'ticket.commented' : 'ticket.reply_created', $user);
$this->afterCommentCreated($ticket, $user, $comment, $files, $internal, $parentId);
return $comment->fresh(['user', 'attachments']) ?? $comment;
});
}
private function resolveParentComment(Ticket $ticket, ?int $parentId): ?Comment
{
return $parentId === null
? null
: Comment::query()->where('ticket_id', $ticket->id)->findOrFail($parentId);
}
/** @return array<string, mixed> */
private function commentPayload(Ticket $ticket, User $user, string $body, bool $internal, ?Comment $parent, ?int $parentId, ?string $subject): array
{
$guid = (string) Str::uuid();
$unid = strtoupper(hash('sha256', $guid . '|' . $ticket->id . '|' . microtime(true)));
$parentUnid = $parent?->unid;
$payload = [
'parent_id' => $parentId,
'user_id' => $user->id,
'body' => $body,
'body_text' => trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: ''),
'is_internal' => $internal,
'guid' => $guid,
'unid' => $unid,
'parent_unid' => $parentUnid,
'root_unid' => $parent?->root_unid ?: $parentUnid ?: $unid,
'originator_id' => $unid . ':1',
'sync_sequence' => 1,
'sync_hash' => hash('sha256', $body),
'source_system' => 'wms-helpdesk',
];
if (Schema::hasColumn('helpdesk_comments', 'subject')) {
$defaultSubject = $parentId === null ? 'Response' : 'RE: Response';
$payload['subject'] = trim((string) ($subject ?: $defaultSubject));
}
return $payload;
}
/** @param array<int, UploadedFile> $files */
private function afterCommentCreated(Ticket $ticket, User $user, Comment $comment, array $files, bool $internal, ?int $parentId): void
{
$ticket->forceFill(['last_activity_at' => now()])->save();
if (! $internal) {
$this->sla->recordPublicResponse($ticket, $user);
}
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
$event = $parentId === null ? 'ticket.commented' : 'ticket.reply_created';
$freshTicket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket;
$this->workflow->dispatch($freshTicket, $event, $user);
}
/**
* Close a ticket.
*/