66 lines
1.9 KiB
PHP
66 lines
1.9 KiB
PHP
<?php
|
|
|
|
/**
|
|
* TicketReferenceService.php
|
|
*
|
|
* Generates collision-resistant Helpdesk ticket references.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Services;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
|
|
final class TicketReferenceService
|
|
{
|
|
/**
|
|
* Build the next human-readable ticket reference for the current year.
|
|
*
|
|
* The lookup uses the highest numeric suffix, not the latest database id,
|
|
* because imported, restored, or manually adjusted tickets may not be in
|
|
* reference order. On PostgreSQL an advisory transaction lock is also used
|
|
* so two users cannot generate the same reference at the same time.
|
|
*/
|
|
public function next(): string
|
|
{
|
|
$year = now()->format('Y');
|
|
$prefix = 'HD-'.$year.'-';
|
|
|
|
$this->lockReferenceSeries($prefix);
|
|
|
|
/** @var list<string> $existingReferences */
|
|
$existingReferences = Ticket::query()
|
|
->withoutGlobalScopes()
|
|
->withTrashed()
|
|
->where('reference', 'like', $prefix.'%')
|
|
->pluck('reference')
|
|
->filter(static fn (mixed $reference): bool => is_string($reference) && preg_match('/^'.preg_quote($prefix, '/').'\d+$/', $reference) === 1)
|
|
->values()
|
|
->all();
|
|
|
|
$highestSuffix = 0;
|
|
|
|
foreach ($existingReferences as $existingReference) {
|
|
$highestSuffix = max($highestSuffix, (int) substr($existingReference, strlen($prefix)));
|
|
}
|
|
|
|
return $prefix.str_pad((string) ($highestSuffix + 1), 5, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
/**
|
|
* Lock the current ticket reference series for the active transaction.
|
|
*/
|
|
private function lockReferenceSeries(string $prefix): void
|
|
{
|
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
|
return;
|
|
}
|
|
|
|
DB::select('select pg_advisory_xact_lock(hashtext(?))', [
|
|
'helpdesk-ticket-reference:'.$prefix,
|
|
]);
|
|
}
|
|
}
|