diff --git a/app/Http/Controllers/Repuve/CancellationController.php b/app/Http/Controllers/Repuve/CancellationController.php index ef5c554..e537a61 100644 --- a/app/Http/Controllers/Repuve/CancellationController.php +++ b/app/Http/Controllers/Repuve/CancellationController.php @@ -22,7 +22,7 @@ public function cancelarConstancia(CancelConstanciaRequest $request) DB::beginTransaction(); // Buscar el expediente con sus relaciones - $record = Record::with('vehicle.tag')->findOrFail($request->record_id); + $record = Record::with('vehicle.tag.status')->findOrFail($request->record_id); $vehicle = $record->vehicle; $tag = $vehicle->tag; @@ -34,9 +34,9 @@ public function cancelarConstancia(CancelConstanciaRequest $request) } // Validar que el tag está en estado activo - if ($tag->status !== 'assigned') { + if (!$tag->isAssigned()) { return ApiResponse::BAD_REQUEST->response([ - 'message' => 'El tag ya no está activo. Status actual: ' . $tag->status + 'message' => 'El tag ya no está activo. Status actual: ' . $tag->status->name ]); } @@ -59,10 +59,7 @@ public function cancelarConstancia(CancelConstanciaRequest $request) ]); // Actualizar estado del tag a 'cancelled' y desasignar vehículo - $tag->update([ - 'status' => 'cancelled', - 'vehicle_id' => null, - ]); + $tag->markAsCancelled(); DB::commit(); @@ -78,8 +75,8 @@ public function cancelarConstancia(CancelConstanciaRequest $request) 'tag' => [ 'id' => $tag->id, 'folio' => $tag->folio, - 'old_status' => 'assigned', - 'new_status' => 'cancelled', + 'old_status' => $tag->status->name, + 'new_status' => 'Cancelado', ], 'cancellation_reason' => $request->cancellation_reason, 'cancellation_observations' => $request->cancellation_observations, diff --git a/app/Http/Controllers/Repuve/InscriptionController.php b/app/Http/Controllers/Repuve/InscriptionController.php index 2eb9486..465828f 100644 --- a/app/Http/Controllers/Repuve/InscriptionController.php +++ b/app/Http/Controllers/Repuve/InscriptionController.php @@ -279,7 +279,7 @@ public function vehicleInscription(VehicleStoreRequest $request) 'tag' => [ 'id' => $record->vehicle->tag->id, 'folio' => $record->vehicle->tag->folio, - 'status' => $record->vehicle->tag->status, + 'status' => $record->vehicle->tag->status->name, ], 'files' => $uploadedFiles, 'total_files' => count($uploadedFiles), @@ -377,7 +377,8 @@ public function searchRecord(Request $request) $records = Record::with([ 'vehicle:id,owner_id,placa,niv,marca,linea,modelo,color', 'vehicle.owner:id,name,paternal,maternal,rfc', - 'vehicle.tag:id,vehicle_id,folio,tag_number,status', + 'vehicle.tag:id,vehicle_id,folio,tag_number,status_id', + 'vehicle.tag.status:id,code,name', 'files:id,record_id,name_id,path,md5', 'files.catalogName:id,name', 'user:id,name,email', diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php index edfcb63..d1817dc 100644 --- a/app/Http/Controllers/Repuve/UpdateController.php +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -14,6 +14,8 @@ use App\Models\Owner; use App\Models\Error; use App\Models\CatalogNameImg; +use App\Models\Tag; +use App\Models\VehicleTagLog; use App\Services\RepuveService; use App\Services\PadronEstatalService; use Exception; @@ -55,7 +57,7 @@ public function vehicleData(Request $request) $record = Record::with([ 'vehicle:id,owner_id,placa,niv,marca,linea,modelo,color', 'vehicle.owner:id,name,paternal,maternal,rfc', - 'vehicle.tag:id,vehicle_id,folio,tag_number,status', + 'vehicle.tag:id,vehicle_id,folio,tag_number,status_id', 'files:id,record_id,name_id,path,md5', 'files.catalogName:id,name', 'user:id,name,email', @@ -155,14 +157,28 @@ public function vehicleUpdate(VehicleUpdateRequest $request) } $vehicle = $record->vehicle; - $tag = $vehicle->tag; + $currentTag = $vehicle->tag; + $newTag = Tag::where('tag_number', $tagNumber)->first(); - if (!$tag || $tag->tag_number !== $tagNumber) { - return ApiResponse::BAD_REQUEST->response([ - 'message' => 'El tag_number no coincide con el registrado en el expediente', + if (!$newTag) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontró el tag con el tag_number proporcionado', + 'tag_number' => $tagNumber, ]); } + $isTagReplacement = false; + if($currentTag && $currentTag->id !== $newTag->id) { + if(!$newTag->isAvailable()) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El tag proporcionado no está disponible para asignación', + 'tag_number' => $tagNumber, + 'current_status' => $newTag->status->name, + ]); + } + $isTagReplacement = true; + } + if ($vehicle->niv !== $niv) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El NIV no coincide con el registrado en el expediente', @@ -240,6 +256,26 @@ public function vehicleUpdate(VehicleUpdateRequest $request) ]); } + $tagReplacementLog = null; + if($isTagReplacement) { + $tagReplacementLog = VehicleTagLog::create([ + 'vehicle_id' => $vehicle->id, + 'tag_id' => $currentTag->id, + 'cancellation_reason' => 'Otro', + 'cancellation_observations' => 'Reemplazo automático al actualizar datos del vehículo: '. $newTag->tag_number, + 'cancellation_at' => now(), + 'cancelled_by' => auth()->id(), + ]); + + $currentTag->markAsCancelled(); + + $newTag->markAsAssigned($vehicle->id, $folio); + + $tag = $newTag; + }else{ + $tag = $currentTag; + } + $uploadedFiles = []; $replacedFiles = []; @@ -310,7 +346,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request) } // Solo enviar a REPUVE Nacional si hay cambios - if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0) { + if ($hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0|| $isTagReplacement) { //Envio de datos $apiResponse = $this->sendToRepuveNacional($niv); $apiResponse["repuve_response"]["folio_ci"] = $record->folio; @@ -361,13 +397,15 @@ public function vehicleUpdate(VehicleUpdateRequest $request) DB::commit(); - $sentToRepuve = $hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0; + $sentToRepuve = $hasVehicleChanges || $hasOwnerChanges || count($uploadedFiles) > 0 || $isTagReplacement; $message = 'Vehículo actualizado exitosamente'; if (!$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles)) { $message = 'No se detectaron cambios. Los datos ya estaban actualizados.'; - } elseif (!$hasVehicleChanges && !$hasOwnerChanges) { + } elseif (!$hasVehicleChanges && !$hasOwnerChanges && !$isTagReplacement) { $message = 'Solo se actualizaron archivos. Los datos del vehículo/propietario no cambiaron.'; + } elseif ($isTagReplacement && !$hasVehicleChanges && !$hasOwnerChanges && empty($uploadedFiles)) { + $message = 'Tag reemplazado exitosamente. Los datos del vehículo/propietario no cambiaron.'; } return ApiResponse::OK->response([ @@ -378,7 +416,20 @@ public function vehicleUpdate(VehicleUpdateRequest $request) 'vehicle_updated' => $hasVehicleChanges, 'owner_updated' => $hasOwnerChanges, 'files_uploaded' => count($uploadedFiles) > 0, + 'tag_replaced' => $isTagReplacement, ], + 'tag_replacement' => $isTagReplacement ? [ + 'old_tag' => [ + 'id' => $currentTag->id, + 'tag_number' => $currentTag->tag_number, + 'status' => $currentTag->status->name, + ], + 'new_tag' => [ + 'id' => $newTag->id, + 'tag_number' => $newTag->tag_number, + 'status' => $newTag->status->name, + ], + ] : null, 'record' => [ 'id' => $record->id, 'folio' => $record->folio, @@ -400,7 +451,7 @@ public function vehicleUpdate(VehicleUpdateRequest $request) 'tag' => [ 'id' => $tag->id, 'tag_number' => $tag->tag_number, - 'status' => $tag->status, + 'status' => $tag->status->name, ], 'uploaded_files' => $uploadedFiles, 'replaced_count' => count($replacedFiles), diff --git a/app/Models/CatalogTagStatus.php b/app/Models/CatalogTagStatus.php new file mode 100644 index 0000000..a27e180 --- /dev/null +++ b/app/Models/CatalogTagStatus.php @@ -0,0 +1,62 @@ + + * + * @version 1.0.0 + */ +class CatalogTagStatus extends Model +{ + // Constantes de códigos de estatus + const CODE_AVAILABLE = 'available'; + const CODE_ASSIGNED = 'assigned'; + const CODE_CANCELLED = 'cancelled'; + + protected $table = 'catalog_tag_status'; + + protected $fillable = [ + 'code', + 'name', + 'description', + 'active', + ]; + + protected function casts(): array + { + return [ + 'active' => 'boolean', + ]; + } + + /** + * Tags que tienen este estatus + */ + public function tags() + { + return $this->hasMany(Tag::class, 'status_id'); + } + + /** + * Scope para obtener solo estatus activos + */ + public function scopeActive($query) + { + return $query->where('active', true); + } + + /** + * Scope para buscar por código + */ + public function scopeByCode($query, string $code) + { + return $query->where('code', $code); + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php index f6a4f39..bea7411 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -20,7 +20,7 @@ class Tag extends Model 'tag_number', 'vehicle_id', 'package_id', - 'status', + 'status_id', ]; public function vehicle() @@ -33,6 +33,11 @@ public function package() return $this->belongsTo(Package::class); } + public function status() + { + return $this->belongsTo(CatalogTagStatus::class, 'status_id'); + } + public function vehicleTagLogs() { return $this->hasMany(VehicleTagLog::class); @@ -48,10 +53,25 @@ public function scanHistories() */ public function markAsAssigned(int $vehicleId, string $folio): void { + $statusAssigned = CatalogTagStatus::where('code', self::STATUS_ASSIGNED)->first(); + $this->update([ 'vehicle_id' => $vehicleId, 'folio' => $folio, - 'status' => self::STATUS_ASSIGNED, + 'status_id' => $statusAssigned->id, + ]); + } + + /** + * Marcar tag como cancelado + */ + public function markAsCancelled(): void + { + $statusCancelled = CatalogTagStatus::where('code', self::STATUS_CANCELLED)->first(); + + $this->update([ + 'status_id' => $statusCancelled->id, + 'vehicle_id' => null, ]); } @@ -60,7 +80,7 @@ public function markAsAssigned(int $vehicleId, string $folio): void */ public function isAvailable(): bool { - return $this->status === self::STATUS_AVAILABLE; + return $this->status->code === self::STATUS_AVAILABLE; } /** @@ -68,7 +88,7 @@ public function isAvailable(): bool */ public function isAssigned(): bool { - return $this->status === self::STATUS_ASSIGNED; + return $this->status->code === self::STATUS_ASSIGNED; } /** @@ -76,14 +96,14 @@ public function isAssigned(): bool */ public function isCancelled(): bool { - return $this->status === self::STATUS_CANCELLED; + return $this->status->code === self::STATUS_CANCELLED; } /** - * Verificar si el tag está perdido + * Verificar si el tag está perdido (ahora es parte de cancelado) */ public function isLost(): bool { - return $this->status === self::STATUS_LOST; + return $this->status->code === self::STATUS_CANCELLED; } } diff --git a/database/migrations/2025_11_22_104814_create_catalog_tag_status_table.php b/database/migrations/2025_11_22_104814_create_catalog_tag_status_table.php new file mode 100644 index 0000000..3ec5c29 --- /dev/null +++ b/database/migrations/2025_11_22_104814_create_catalog_tag_status_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('code')->unique(); + $table->string('name'); + $table->text('description')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('catalog_tag_status'); + } +}; diff --git a/database/migrations/2025_11_22_104815_modify_tags_table_change_status_to_foreign_key.php b/database/migrations/2025_11_22_104815_modify_tags_table_change_status_to_foreign_key.php new file mode 100644 index 0000000..79da380 --- /dev/null +++ b/database/migrations/2025_11_22_104815_modify_tags_table_change_status_to_foreign_key.php @@ -0,0 +1,59 @@ +unsignedBigInteger('status_id')->nullable()->after('package_id'); + }); + + DB::table('tags')->where('status', 'available')->update(['status_id' => 1]); + DB::table('tags')->where('status', 'assigned')->update(['status_id' => 2]); + DB::table('tags')->where('status', 'cancelled')->update(['status_id' => 3]); + DB::table('tags')->where('status', 'lost')->update(['status_id' => 3]); + + DB::table('tags')->whereNull('status_id')->update(['status_id' => 1]); + + Schema::table('tags', function (Blueprint $table) { + $table->dropColumn('status'); + }); + + DB::statement('ALTER TABLE tags MODIFY status_id BIGINT UNSIGNED NOT NULL DEFAULT 1'); + + Schema::table('tags', function (Blueprint $table) { + $table->foreign('status_id')->references('id')->on('catalog_tag_status')->onDelete('restrict'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Restaurar el enum + Schema::table('tags', function (Blueprint $table) { + $table->enum('status', ['available', 'assigned', 'cancelled', 'lost'])->default('available')->after('package_id'); + }); + + // Mapear de vuelta los IDs a enum + DB::table('tags')->where('status_id', 1)->update(['status' => 'available']); + DB::table('tags')->where('status_id', 2)->update(['status' => 'assigned']); + DB::table('tags')->where('status_id', 3)->update(['status' => 'cancelled']); + + // Eliminar la foreign key y columna status_id + Schema::table('tags', function (Blueprint $table) { + $table->dropForeign(['status_id']); + $table->dropColumn('status_id'); + }); + } +}; diff --git a/database/seeders/CatalogTagStatusSeeder.php b/database/seeders/CatalogTagStatusSeeder.php new file mode 100644 index 0000000..4f91d32 --- /dev/null +++ b/database/seeders/CatalogTagStatusSeeder.php @@ -0,0 +1,52 @@ + + * + * @version 1.0.0 + */ +class CatalogTagStatusSeeder extends Seeder +{ + /** + * Ejecutar sembrado de base de datos + */ + public function run(): void + { + $statuses = [ + [ + 'code' => 'available', + 'name' => 'Disponible', + 'description' => 'Tag disponible para ser asignado a un vehículo', + 'active' => true, + ], + [ + 'code' => 'assigned', + 'name' => 'Asignado', + 'description' => 'Tag asignado a un vehículo', + 'active' => true, + ], + [ + 'code' => 'cancelled', + 'name' => 'Cancelado', + 'description' => 'Tag cancelado (incluye perdidos, dañados o reemplazados)', + 'active' => true, + ], + ]; + + foreach ($statuses as $status) { + CatalogTagStatus::updateOrCreate( + ['code' => $status['code']], + $status + ); + } + } +} diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index 9bec541..8dab9ad 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -26,6 +26,6 @@ public function run(): void $this->call(CatalogNameImgSeeder::class); $this->call(MunicipalitySeeder::class); $this->call(ModuleSeeder::class); - + $this->call(CatalogTagStatusSeeder::class); } }