Commit 2ee9537f authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix unique constraint violations on re-creation after delete

Add ManglesUniqueOnDelete trait that appends a timestamp suffix to unique
fields (code, slug, sku, employee_number) when a record is soft-deleted,
freeing the value for new records. Also make group code generation
collision-resistant by checking withTrashed().
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 497401cb
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Shared\Traits\Auditable; use App\Domain\Shared\Traits\Auditable;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
...@@ -16,7 +17,9 @@ ...@@ -16,7 +17,9 @@
class Facility extends Model class Facility extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable; use BelongsToAcademy, HasUuid, SoftDeletes, Auditable, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['code'];
protected $fillable = [ protected $fillable = [
'academy_id', 'academy_id',
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use App\Domain\HR\Enums\SalaryFrequency; use App\Domain\HR\Enums\SalaryFrequency;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
...@@ -17,7 +18,9 @@ ...@@ -17,7 +18,9 @@
class Employee extends Model class Employee extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes; use BelongsToAcademy, HasUuid, SoftDeletes, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['employee_number'];
protected $fillable = [ protected $fillable = [
'academy_id', 'academy_id',
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
...@@ -12,7 +13,9 @@ ...@@ -12,7 +13,9 @@
class Branch extends Model class Branch extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes; use BelongsToAcademy, HasUuid, SoftDeletes, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['code'];
protected $fillable = [ protected $fillable = [
'academy_id', 'name', 'name_ar', 'code', 'academy_id', 'name', 'name_ar', 'code',
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Shared\Traits\Auditable; use App\Domain\Shared\Traits\Auditable;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
...@@ -12,7 +13,9 @@ ...@@ -12,7 +13,9 @@
class Kit extends Model class Kit extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable; use BelongsToAcademy, HasUuid, SoftDeletes, Auditable, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['sku'];
protected $fillable = [ protected $fillable = [
'academy_id', 'academy_id',
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Inventory\Enums\WarehouseType; use App\Domain\Inventory\Enums\WarehouseType;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
...@@ -14,7 +15,9 @@ ...@@ -14,7 +15,9 @@
class Warehouse extends Model class Warehouse extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes; use BelongsToAcademy, HasUuid, SoftDeletes, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['code'];
protected $fillable = [ protected $fillable = [
'academy_id', 'academy_id',
......
<?php
namespace App\Domain\Shared\Traits;
use Illuminate\Support\Str;
trait ManglesUniqueOnDelete
{
public static function bootManglesUniqueOnDelete(): void
{
static::deleting(function ($model) {
$suffix = '_del_' . now()->format('ymdHis');
foreach ($model->getUniqueFieldsToMangle() as $field) {
if ($model->{$field} !== null) {
$model->{$field} = Str::limit($model->{$field}, 200, '') . $suffix;
}
}
$model->saveQuietly();
});
}
public function getUniqueFieldsToMangle(): array
{
return $this->uniqueFieldsToMangle ?? [];
}
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Shared\Traits\Auditable; use App\Domain\Shared\Traits\Auditable;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use App\Domain\Training\Enums\GroupStatus; use App\Domain\Training\Enums\GroupStatus;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
...@@ -15,7 +16,9 @@ ...@@ -15,7 +16,9 @@
class TrainingGroup extends Model class TrainingGroup extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable; use BelongsToAcademy, HasUuid, SoftDeletes, Auditable, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['code'];
protected $fillable = [ protected $fillable = [
'academy_id', 'training_program_id', 'branch_id', 'academy_id', 'training_program_id', 'branch_id',
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Shared\Traits\Auditable; use App\Domain\Shared\Traits\Auditable;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use App\Domain\Training\Enums\ProgramStatus; use App\Domain\Training\Enums\ProgramStatus;
use App\Domain\Training\Enums\RenewalPolicy; use App\Domain\Training\Enums\RenewalPolicy;
use App\Models\User; use App\Models\User;
...@@ -16,7 +17,9 @@ ...@@ -16,7 +17,9 @@
class TrainingProgram extends Model class TrainingProgram extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable; use BelongsToAcademy, HasUuid, SoftDeletes, Auditable, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['slug'];
protected $fillable = [ protected $fillable = [
'academy_id', 'activity_id', 'branch_id', 'academy_id', 'activity_id', 'branch_id',
......
...@@ -15,7 +15,7 @@ public function create(array $data, User $actor): TrainingProgram ...@@ -15,7 +15,7 @@ public function create(array $data, User $actor): TrainingProgram
{ {
return DB::transaction(function () use ($data, $actor) { return DB::transaction(function () use ($data, $actor) {
$slug = Str::limit($data['slug'] ?? Str::slug($data['name']), 90, ''); $slug = Str::limit($data['slug'] ?? Str::slug($data['name']), 90, '');
$exists = TrainingProgram::where('slug', $slug)->exists(); $exists = TrainingProgram::withTrashed()->where('slug', $slug)->exists();
if ($exists) { if ($exists) {
$slug = Str::limit($slug, 85, '') . '-' . Str::random(4); $slug = Str::limit($slug, 85, '') . '-' . Str::random(4);
} }
...@@ -34,7 +34,14 @@ public function create(array $data, User $actor): TrainingProgram ...@@ -34,7 +34,14 @@ public function create(array $data, User $actor): TrainingProgram
private function createDefaultGroup(TrainingProgram $program, User $actor): TrainingGroup private function createDefaultGroup(TrainingProgram $program, User $actor): TrainingGroup
{ {
$programName = $program->name_ar ?: $program->name; $programName = $program->name_ar ?: $program->name;
$code = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $program->name ?: $program->name_ar), 0, 6)) . '01'; $baseCode = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $program->name ?: $program->name_ar), 0, 6)) . '01';
$code = $baseCode;
$attempt = 0;
while (TrainingGroup::withTrashed()->where('academy_id', $program->academy_id)->where('code', $code)->exists()) {
$attempt++;
$code = $baseCode . '-' . $attempt;
}
return TrainingGroup::create([ return TrainingGroup::create([
'academy_id' => $program->academy_id, 'academy_id' => $program->academy_id,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment