Squashed commit of the following:

commit 6ae625d4fdc11989950ef0678db62fdb52be7c8c
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 09:46:18 2026 -0300

    refactor(tickets): enhance search functionality for tickets by client names and formatted amounts

commit 098e525d32be9cb6487bc01a3d120aa0f2386a15
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 09:09:30 2026 -0300

    fix(tickets): protect referenced purchase items

commit 210db17f2f6a6a4a509297b773a2b5d3e3149a3d
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 08:59:24 2026 -0300

    test(tickets): cover purchase item references

commit 6a004713c4bad622dfa5e26394458925db1cb5a7
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Sep 2 08:59:02 2026 -0300

    refactor(tickets): reference purchase items directly
This commit is contained in:
2026-09-02 09:47:04 -03:00
parent 836aa1afd7
commit ee906d2d2e
18 changed files with 504 additions and 85 deletions

View File

@@ -0,0 +1,94 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->foreignId('source_purchase_item_id')
->nullable()
->after('source_purchase_id')
->constrained('compra_items')
->cascadeOnUpdate()
->nullOnDelete();
});
DB::table('tickets')
->whereNotNull('source_purchase_id')
->whereNull('source_purchase_item_id')
->orderBy('id')
->chunkById(500, function (Collection $tickets): void {
$purchaseIds = $tickets->pluck('source_purchase_id')->unique()->values();
$itemsByPurchase = DB::table('compra_items')
->whereIn('compra_id', $purchaseIds)
->get(['id', 'compra_id', 'source_catalog_item_id', 'source_variant_id'])
->groupBy('compra_id');
$bundleIds = $itemsByPurchase->flatten(1)
->pluck('source_catalog_item_id')
->filter()
->unique()
->values();
$componentsByBundle = DB::table('bundle_components')
->whereIn('bundle_catalog_item_id', $bundleIds)
->get([
'bundle_catalog_item_id',
'component_catalog_item_id',
'component_variant_id',
])
->groupBy('bundle_catalog_item_id');
foreach ($tickets as $ticket) {
$candidates = collect($itemsByPurchase->get($ticket->source_purchase_id, []))
->filter(function (object $item) use ($ticket, $componentsByBundle): bool {
if ($this->sameCatalogTarget($item, $ticket)) {
return true;
}
return collect($componentsByBundle->get($item->source_catalog_item_id, []))
->contains(fn (object $component): bool => $this->sameBundleTarget($component, $ticket));
})
->pluck('id')
->unique()
->values();
if ($candidates->count() === 1) {
DB::table('tickets')->where('id', $ticket->id)->update([
'source_purchase_item_id' => $candidates->sole(),
]);
}
}
});
}
public function down(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->dropConstrainedForeignId('source_purchase_item_id');
});
}
private function sameCatalogTarget(object $item, object $ticket): bool
{
return (int) $item->source_catalog_item_id === (int) $ticket->source_catalog_item_id
&& $this->sameNullableId($item->source_variant_id, $ticket->source_variant_id);
}
private function sameBundleTarget(object $component, object $ticket): bool
{
return (int) $component->component_catalog_item_id === (int) $ticket->source_catalog_item_id
&& $this->sameNullableId($component->component_variant_id, $ticket->source_variant_id);
}
private function sameNullableId(mixed $left, mixed $right): bool
{
return $left === null || $right === null
? $left === null && $right === null
: (int) $left === (int) $right;
}
};

View File

@@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$unresolved = DB::table('tickets')
->whereNotNull('source_purchase_id')
->whereNull('source_purchase_item_id')
->orderBy('id')
->limit(20)
->pluck('id');
if ($unresolved->isNotEmpty()) {
throw new RuntimeException(
'No se puede eliminar tickets.source_purchase_id: hay tickets sin un compra_item inequívoco. '
.'IDs: '.$unresolved->implode(', ')
);
}
$inconsistent = DB::table('tickets')
->join('compra_items', 'compra_items.id', '=', 'tickets.source_purchase_item_id')
->whereNotNull('tickets.source_purchase_id')
->whereColumn('tickets.source_purchase_id', '!=', 'compra_items.compra_id')
->orderBy('tickets.id')
->limit(20)
->pluck('tickets.id');
if ($inconsistent->isNotEmpty()) {
throw new RuntimeException(
'No se puede eliminar tickets.source_purchase_id: hay referencias de compra inconsistentes. '
.'IDs: '.$inconsistent->implode(', ')
);
}
Schema::table('tickets', function (Blueprint $table): void {
$table->dropConstrainedForeignId('source_purchase_id');
});
}
public function down(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->foreignId('source_purchase_id')
->nullable()
->after('description')
->constrained('compras')
->cascadeOnUpdate()
->nullOnDelete();
});
DB::table('tickets')
->whereNotNull('source_purchase_item_id')
->orderBy('id')
->chunkById(500, function (Collection $tickets): void {
$purchaseIdsByItem = DB::table('compra_items')
->whereIn('id', $tickets->pluck('source_purchase_item_id'))
->pluck('compra_id', 'id');
foreach ($tickets as $ticket) {
DB::table('tickets')->where('id', $ticket->id)->update([
'source_purchase_id' => $purchaseIdsByItem->get($ticket->source_purchase_item_id),
]);
}
});
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->dropForeign(['source_purchase_item_id']);
});
Schema::table('tickets', function (Blueprint $table): void {
$table->foreign('source_purchase_item_id')
->references('id')
->on('compra_items')
->cascadeOnUpdate()
->restrictOnDelete();
});
}
public function down(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->dropForeign(['source_purchase_item_id']);
});
Schema::table('tickets', function (Blueprint $table): void {
$table->foreign('source_purchase_item_id')
->references('id')
->on('compra_items')
->cascadeOnUpdate()
->nullOnDelete();
});
}
};