Repo rebuild
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TicketReferenceService.php
|
||||
*
|
||||
* Generates collision-resistant Helpdesk ticket references.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class TicketReferenceService
|
||||
{
|
||||
/**
|
||||
* Build the next human-readable ticket reference for the current year.
|
||||
*
|
||||
* The preferred strategy uses a dedicated sequence row locked inside the
|
||||
* ticket creation transaction. The initial sequence value is backfilled from
|
||||
* all existing ticket references, including soft-deleted tickets, so a
|
||||
* previously used reference can never be reused while the unique index still
|
||||
* protects it.
|
||||
*/
|
||||
public function next(): string
|
||||
{
|
||||
$year = (int) now()->format('Y');
|
||||
$prefix = 'HD';
|
||||
$seriesPrefix = $prefix . '-' . $year . '-';
|
||||
|
||||
$this->lockReferenceSeries($seriesPrefix);
|
||||
|
||||
if (Schema::hasTable('helpdesk_ticket_sequences')) {
|
||||
return $this->nextFromSequenceTable($prefix, $year, $seriesPrefix);
|
||||
}
|
||||
|
||||
return $this->nextFromExistingReferences($seriesPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next reference from the durable sequence table.
|
||||
*/
|
||||
private function nextFromSequenceTable(string $prefix, int $year, string $seriesPrefix): string
|
||||
{
|
||||
$sequence = DB::table('helpdesk_ticket_sequences')
|
||||
->where('prefix', $prefix)
|
||||
->where('year', $year)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (! $sequence) {
|
||||
DB::table('helpdesk_ticket_sequences')->insert([
|
||||
'prefix' => $prefix,
|
||||
'year' => $year,
|
||||
'next_number' => $this->highestExistingSuffix($seriesPrefix) + 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$sequence = DB::table('helpdesk_ticket_sequences')
|
||||
->where('prefix', $prefix)
|
||||
->where('year', $year)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
}
|
||||
|
||||
$nextNumber = max((int) $sequence->next_number, $this->highestExistingSuffix($seriesPrefix) + 1);
|
||||
|
||||
DB::table('helpdesk_ticket_sequences')
|
||||
->where('id', $sequence->id)
|
||||
->update([
|
||||
'next_number' => $nextNumber + 1,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $seriesPrefix . str_pad((string) $nextNumber, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for installations that have not run the sequence migration yet.
|
||||
*/
|
||||
private function nextFromExistingReferences(string $seriesPrefix): string
|
||||
{
|
||||
return $seriesPrefix . str_pad((string) ($this->highestExistingSuffix($seriesPrefix) + 1), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the highest used suffix across active and soft-deleted tickets.
|
||||
*/
|
||||
private function highestExistingSuffix(string $seriesPrefix): int
|
||||
{
|
||||
/** @var list<string> $existingReferences */
|
||||
$existingReferences = Ticket::query()
|
||||
->withoutGlobalScopes()
|
||||
->withTrashed()
|
||||
->where('reference', 'like', $seriesPrefix . '%')
|
||||
->pluck('reference')
|
||||
->filter(static fn (mixed $reference): bool => is_string($reference) && preg_match('/^' . preg_quote($seriesPrefix, '/') . '\d+$/', $reference) === 1)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$highestSuffix = 0;
|
||||
|
||||
foreach ($existingReferences as $existingReference) {
|
||||
$highestSuffix = max($highestSuffix, (int) substr($existingReference, strlen($seriesPrefix)));
|
||||
}
|
||||
|
||||
return $highestSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the current ticket reference series for the active transaction.
|
||||
*/
|
||||
private function lockReferenceSeries(string $seriesPrefix): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::select('select pg_advisory_xact_lock(hashtext(?))', [
|
||||
'helpdesk-ticket-reference:' . $seriesPrefix,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user