Compare commits
No commits in common. "generar-excel" and "main" have entirely different histories.
generar-ex
...
main
@ -36,6 +36,7 @@ DB_USERNAME=notsoweb
|
||||
DB_PASSWORD=
|
||||
PMA_PORT=8081 # Puerto para phpMyAdmin
|
||||
|
||||
REDIS_PORT=6379 # Puerto para Redis
|
||||
NGINX_PORT=8080 # Puerto para Nginx
|
||||
|
||||
SESSION_DRIVER=database
|
||||
@ -74,14 +75,6 @@ AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
# REPUVE FEDERAL
|
||||
REPUVE_FED_BASE_URL=
|
||||
REPUVE_FED_USERNAME=
|
||||
REPUVE_FED_PASSWORD=
|
||||
|
||||
# REPUVE ESTATAL
|
||||
REPUVE_EST_URL=
|
||||
|
||||
REVERB_APP_ID=
|
||||
REVERB_APP_KEY=
|
||||
REVERB_APP_SECRET=
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,7 +8,6 @@
|
||||
/public/vendor
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/storage/app/backup
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /var/www/repuve-backend-v1/public;
|
||||
root /var/www/golscontrols/public;
|
||||
index index.php index.html;
|
||||
|
||||
# Logging
|
||||
@ -17,7 +17,7 @@ server {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass repuve-backend:9000;
|
||||
fastcgi_pass golscontrols:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
# Timeouts importantes para evitar errores 500
|
||||
@ -45,17 +45,17 @@ server {
|
||||
|
||||
# Handle storage files (Laravel storage link)
|
||||
location /storage {
|
||||
alias /var/www/repuve-backend-v1/storage/app/public;
|
||||
alias /var/www/golscontrols/storage/app;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /profile {
|
||||
alias /var/www/repuve-backend-v1/storage/app/profile;
|
||||
alias /var/www/golscontrols/storage/app/profile;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /images {
|
||||
alias /var/www/repuve-backend-v1/storage/app/images;
|
||||
alias /var/www/golscontrols/storage/app/images;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class BackupCron extends Command
|
||||
{
|
||||
|
||||
protected $signature = 'backup:cron';
|
||||
protected $description = 'Respaldo automático de la base de datos';
|
||||
|
||||
/**
|
||||
* Ejecutar comando
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$filename = 'backup-' . date('Y-m-d_H-i-s') . '.sql';
|
||||
$containerPath = '/tmp/' . $filename;
|
||||
$finalPath = storage_path('app/backup/' . $filename);
|
||||
|
||||
// Asegurar que existe el directorio de backup
|
||||
$backupDir = storage_path('app/backup');
|
||||
if (!file_exists($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
// Crear backup ejecutando mysqldump dentro del contenedor MySQL
|
||||
$dumpCommand = sprintf(
|
||||
'docker exec repuve-backend-v1-mysql-1 sh -c "mysqldump --no-tablespaces -u%s -p%s %s > %s" 2>&1',
|
||||
env('DB_USERNAME'),
|
||||
env('DB_PASSWORD'),
|
||||
env('DB_DATABASE'),
|
||||
$containerPath
|
||||
);
|
||||
exec($dumpCommand, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Error al crear el backup en MySQL');
|
||||
$this->error('Código: ' . $returnCode);
|
||||
if (!empty($output)) {
|
||||
$this->error('Salida: ' . implode("\n", $output));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Copiar del contenedor MySQL a /tmp del host
|
||||
$tempHostPath = '/tmp/' . $filename;
|
||||
$copyCommand = sprintf(
|
||||
'docker cp repuve-backend-v1-mysql-1:%s %s 2>&1',
|
||||
$containerPath,
|
||||
$tempHostPath
|
||||
);
|
||||
exec($copyCommand, $copyOutput, $copyReturnCode);
|
||||
|
||||
if ($copyReturnCode !== 0) {
|
||||
$this->error('Error al copiar el backup');
|
||||
if (!empty($copyOutput)) {
|
||||
$this->error('Salida: ' . implode("\n", $copyOutput));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Mover de /tmp a storage y establecer permisos
|
||||
if (file_exists($tempHostPath)) {
|
||||
rename($tempHostPath, $finalPath);
|
||||
chmod($finalPath, 0644);
|
||||
|
||||
// Limpiar archivo temporal del contenedor MySQL
|
||||
exec('docker exec repuve-backend-v1-mysql-1 rm ' . $containerPath);
|
||||
|
||||
$size = filesize($finalPath);
|
||||
$this->info('Backup creado exitosamente: ' . $filename);
|
||||
$this->info('Tamaño: ' . $this->formatBytes($size));
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
$this->error('Error: el archivo temporal no se creó');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatear bytes a tamaño legible
|
||||
*/
|
||||
private function formatBytes($bytes, $precision = 2)
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= pow(1024, $pow);
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Owner;
|
||||
use App\Models\Vehicle;
|
||||
use App\Models\Tag;
|
||||
use App\Models\CatalogTagStatus;
|
||||
use App\Models\VehicleTagLog;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SeedOwnerVehicle extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'seed:owner-vehicle';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Crear un propietario y un vehículo de ejemplo';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$this->info('Creando propietario...');
|
||||
|
||||
// Crear un propietario
|
||||
$owner = Owner::create([
|
||||
'name' => 'Juan',
|
||||
'paternal' => 'Pérez',
|
||||
'maternal' => 'García',
|
||||
'rfc' => 'PEGJ850101ABC',
|
||||
'curp' => 'PEGJ850101HDFRRN01',
|
||||
'address' => 'Calle Principal 123',
|
||||
'tipopers' => true,
|
||||
'pasaporte' => null,
|
||||
'licencia' => 'LIC123456789',
|
||||
'ent_fed' => 'Ciudad de México',
|
||||
'munic' => 'Benito Juárez',
|
||||
'callep' => 'Calle Principal',
|
||||
'num_ext' => '123',
|
||||
'num_int' => 'A',
|
||||
'colonia' => 'Centro',
|
||||
'cp' => '03100',
|
||||
'telefono' => '5551234567',
|
||||
]);
|
||||
|
||||
$this->info("✓ Propietario creado: {$owner->full_name} (ID: {$owner->id})");
|
||||
|
||||
$this->info('Creando vehículo...');
|
||||
|
||||
// Crear un vehículo asociado al propietario
|
||||
$vehicle = Vehicle::create([
|
||||
'placa' => 'ABC-123',
|
||||
'niv' => '1HGBH41JXMN109186',
|
||||
'marca' => 'Honda',
|
||||
'linea' => 'Civic',
|
||||
'sublinea' => 'Sedan',
|
||||
'modelo' => '2020',
|
||||
'color' => 'Blanco',
|
||||
'numero_motor' => 'ENG123456789',
|
||||
'clase_veh' => 'Automóvil',
|
||||
'tipo_servicio' => 'Particular',
|
||||
'rfv' => 'RFV123456789',
|
||||
'ofcexpedicion' => 'Oficina Central',
|
||||
'fechaexpedicion' => '2020-01-15',
|
||||
'tipo_veh' => 'Sedan',
|
||||
'numptas' => '4',
|
||||
'observac' => 'Vehículo en buen estado',
|
||||
'cve_vehi' => 'CVE001',
|
||||
'tipo_mov' => 'Alta',
|
||||
'owner_id' => $owner->id,
|
||||
]);
|
||||
|
||||
$this->info("✓ Vehículo creado: {$vehicle->marca} {$vehicle->linea} - Placa: {$vehicle->placa} (ID: {$vehicle->id})");
|
||||
$this->info("✓ Asociado al propietario: {$owner->full_name}");
|
||||
|
||||
$this->info('Creando tag...');
|
||||
|
||||
// Obtener el status "available" para crear el tag
|
||||
$statusAvailable = CatalogTagStatus::where('code', Tag::STATUS_AVAILABLE)->first();
|
||||
|
||||
if (!$statusAvailable) {
|
||||
throw new \Exception('No se encontró el status "available" en el catálogo. Ejecuta el seeder CatalogTagStatusSeeder primero.');
|
||||
}
|
||||
|
||||
// Generar un folio único para el tag
|
||||
$folio = str_pad(rand(1, 99999999), 8, '0', STR_PAD_LEFT);
|
||||
|
||||
// Verificar que el folio no exista
|
||||
while (Tag::where('folio', $folio)->exists()) {
|
||||
$folio = str_pad(rand(1, 99999999), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// Crear un tag disponible
|
||||
$tag = Tag::create([
|
||||
'folio' => $folio,
|
||||
'tag_number' => 'TAG' . str_pad(rand(1, 999999), 10, '0', STR_PAD_LEFT),
|
||||
'vehicle_id' => null,
|
||||
'package_id' => null,
|
||||
'module_id' => null,
|
||||
'status_id' => $statusAvailable->id,
|
||||
]);
|
||||
|
||||
$this->info("✓ Tag creado: Folio {$tag->folio} - Tag Number: {$tag->tag_number} (ID: {$tag->id})");
|
||||
|
||||
// Asignar el tag al vehículo
|
||||
$tag->markAsAssigned($vehicle->id, $folio);
|
||||
|
||||
$this->info("✓ Tag asignado al vehículo: {$vehicle->placa}");
|
||||
|
||||
// Crear registro en el log de tags
|
||||
VehicleTagLog::create([
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'tag_id' => $tag->id,
|
||||
'action_type' => 'inscripcion',
|
||||
'performed_by' => null,
|
||||
]);
|
||||
|
||||
$this->info("✓ Registro de log creado");
|
||||
$this->newLine();
|
||||
$this->info('✓ Proceso completado exitosamente!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("✗ Error: " . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EncryptionHelper
|
||||
{
|
||||
/**
|
||||
* Encrypt the given data.
|
||||
*/
|
||||
public static function encryptData($data)
|
||||
{
|
||||
try{
|
||||
return Crypt::encryptString(json_encode($data));
|
||||
}catch(\Exception $e){
|
||||
throw new \RuntimeException("Error al encriptar los datos: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the given data.
|
||||
*/
|
||||
public static function decryptData($encryptedData)
|
||||
{
|
||||
try{
|
||||
$decrypted = Crypt::decryptString($encryptedData);
|
||||
return json_decode($decrypted, true);
|
||||
}catch(DecryptException $e){
|
||||
Log::error('Error al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
}catch(\Exception $e){
|
||||
Log::error('Error inesperado al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function encryptFields(array $data, array $fields)
|
||||
{
|
||||
foreach ($fields as $field){
|
||||
if(isset($data[$field])){
|
||||
$data[$field] = self::encryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function decryptFields(array $data, array $fields)
|
||||
{
|
||||
foreach ($fields as $field){
|
||||
if(isset($data[$field])){
|
||||
$data[$field] = self::decryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ class RoleController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$model = Role::where('name', '!=','developer')->orderBy('description');
|
||||
$model = Role::orderBy('description');
|
||||
|
||||
QuerySupport::queryByKey($model, request(), 'name');
|
||||
|
||||
@ -41,13 +41,9 @@ public function index()
|
||||
*/
|
||||
public function store(RoleStoreRequest $request)
|
||||
{
|
||||
$model = Role::create($request->all());
|
||||
Role::create($request->all());
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Rol creado exitosamente',
|
||||
'id' => $model->id,
|
||||
'name' => $model->name,
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -29,23 +29,12 @@ class UserController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$users = User::whereDoesntHave('roles', function ($query) {
|
||||
$query->where('name', 'developer');
|
||||
})->orderBy('name');
|
||||
$users = User::orderBy('name');
|
||||
|
||||
QuerySupport::queryByKeys($users, ['name', 'email']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
/* 'models' => $users->paginate(config('app.pagination')), */
|
||||
'users' => $users->select([
|
||||
'id',
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'email',
|
||||
'module_id',
|
||||
'deleted_at'
|
||||
])->paginate(config('app.pagination'))
|
||||
'models' => $users->paginate(config('app.pagination'))
|
||||
]);
|
||||
}
|
||||
|
||||
@ -60,10 +49,7 @@ public function store(UserStoreRequest $request)
|
||||
$user->roles()->sync($request->roles);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Usuario actualizado exitosamente',
|
||||
'user' => $user->load(['module', 'roles']),
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,14 +69,7 @@ public function update(UserUpdateRequest $request, User $user)
|
||||
{
|
||||
$user->update($request->all());
|
||||
|
||||
if ($request->has('roles')) {
|
||||
$user->roles()->sync($request->roles);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Usuario actualizado exitosamente',
|
||||
'user' => $user->load(['module', 'roles']),
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,383 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\CancelConstanciaRequest;
|
||||
use App\Models\CatalogCancellationReason;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use App\Models\Record;
|
||||
use App\Models\Tag;
|
||||
use App\Models\TagCancellationLog;
|
||||
use App\Models\VehicleTagLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class CancellationController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Cancelar la constancia/tag
|
||||
*/
|
||||
public function cancelarConstancia(CancelConstanciaRequest $request, $recordId)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Buscar el expediente con sus relaciones
|
||||
$record = Record::with('vehicle.tag.status')->find($recordId);
|
||||
|
||||
if (!$record) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El expediente no existe.',
|
||||
'record_id' => $recordId,
|
||||
]);
|
||||
}
|
||||
|
||||
$vehicle = $record->vehicle;
|
||||
$tag = $vehicle->tag;
|
||||
|
||||
// Validar que el vehículo tiene un tag asignado
|
||||
if (!$tag) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El vehículo no tiene un tag/constancia asignada.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que el tag está en estado activo O cancelado (para permitir sustitución posterior)
|
||||
if (!$tag->isAssigned() && !$tag->isCancelled()) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag debe estar asignado o cancelado. Status actual: ' . $tag->status->name
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que se proporcionen los datos de sustitución
|
||||
if (!$request->filled('new_folio') || !$request->filled('new_tag_number')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Para cancelar la constancia, debe proporcionar: nuevo folio y numero de constancia.',
|
||||
'provided' => [
|
||||
'new_folio' => $request->filled('new_folio'),
|
||||
'new_tag_number' => $request->filled('new_tag_number'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que el tag_number tenga exactamente 17 caracteres
|
||||
if (strlen($request->new_tag_number) !== 32) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag_number debe tener exactamente 32 caracteres',
|
||||
'provided_tag_number' => $request->new_tag_number,
|
||||
'length' => strlen($request->new_tag_number),
|
||||
]);
|
||||
}
|
||||
|
||||
$isSubstitution = true;
|
||||
|
||||
// Guardar información del tag anterior ANTES de cancelarlo
|
||||
$oldTagNumber = $tag->tag_number;
|
||||
$oldFolio = $tag->folio;
|
||||
|
||||
// Crear registro en el log de vehículos
|
||||
if ($tag->isAssigned()) {
|
||||
$cancellationLog = VehicleTagLog::create([
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'tag_id' => $tag->id,
|
||||
'action_type' => 'cancelacion',
|
||||
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||
'cancellation_observations' => $request->cancellation_observations,
|
||||
'cancellation_at' => now(),
|
||||
'cancelled_by' => Auth::id(),
|
||||
'performed_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
// Actualizar estado del tag a 'cancelled'
|
||||
$tag->markAsCancelled();
|
||||
} else {
|
||||
// Si ya estaba cancelado, buscar el último log de cancelación
|
||||
$cancellationLog = VehicleTagLog::where('vehicle_id', $vehicle->id)
|
||||
->where('tag_id', $tag->id)
|
||||
->where('action_type', 'cancelacion')
|
||||
->latest()
|
||||
->first();
|
||||
}
|
||||
|
||||
$newTag = null;
|
||||
$substitutionLog = null;
|
||||
|
||||
if ($isSubstitution) {
|
||||
// Buscar el nuevo tag por folio
|
||||
$newTag = Tag::where('folio', $request->new_folio)->first();
|
||||
|
||||
if (!$newTag) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El tag con el folio proporcionado no existe.',
|
||||
'new_folio' => $request->new_folio,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$newTag->isAvailable()) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El nuevo tag no está disponible para asignación',
|
||||
'new_folio' => $request->new_folio,
|
||||
'current_status' => $newTag->status->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// Asignar tag_number al nuevo tag si no lo tiene
|
||||
if (!$newTag->tag_number) {
|
||||
// Validar que el tag_number no esté usado por otro tag
|
||||
$existingTag = Tag::where('tag_number', $request->new_tag_number)
|
||||
->where('id', '!=', $newTag->id)
|
||||
->first();
|
||||
|
||||
if ($existingTag) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag_number ya está asignado a otro tag.',
|
||||
'new_tag_number' => $request->new_tag_number,
|
||||
'folio_existente' => $existingTag->folio,
|
||||
]);
|
||||
}
|
||||
|
||||
$newTag->tag_number = $request->new_tag_number;
|
||||
$newTag->save();
|
||||
} elseif ($newTag->tag_number !== $request->new_tag_number) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag ya tiene un tag_number diferente asignado.',
|
||||
'assigned_tag_number' => $newTag->tag_number,
|
||||
'provided_tag_number' => $request->new_tag_number,
|
||||
]);
|
||||
}
|
||||
|
||||
// Desasignar el tag viejo para evitar conflicto de unique constraint
|
||||
$tag->update(['vehicle_id' => null]);
|
||||
|
||||
// Asignar el nuevo tag al vehículo (usa el folio del tag encontrado)
|
||||
$newTag->markAsAssigned($vehicle->id, $newTag->folio);
|
||||
|
||||
// Crear log de sustitución
|
||||
$substitutionLog = VehicleTagLog::create([
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'tag_id' => $newTag->id,
|
||||
'action_type' => 'sustitucion',
|
||||
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||
'cancellation_observations' => 'Tag sustituido. Tag anterior: ' . $oldTagNumber . ' (Folio: ' . $oldFolio . '). Motivo: ' . ($request->cancellation_observations ?? ''),
|
||||
'performed_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
// Actualizar el folio del expediente con el folio del nuevo tag
|
||||
$record->update(['folio' => $newTag->folio]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = $isSubstitution
|
||||
? 'Tag cancelado y sustituido exitosamente'
|
||||
: 'Constancia cancelada exitosamente';
|
||||
|
||||
// Agregar alerta si NO hay sustitución
|
||||
$alert = null;
|
||||
if (!$isSubstitution) {
|
||||
$alert = [
|
||||
'type' => 'warning',
|
||||
'message' => 'El tag ha sido cancelado y necesita sustitución',
|
||||
'requires_action' => true,
|
||||
'cancellation_date' => $cancellationLog->cancellation_at->format('d/m/Y H:i:s'),
|
||||
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
||||
];
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => $message,
|
||||
'is_substitution' => $isSubstitution,
|
||||
'alert' => $alert,
|
||||
'cancellation' => [
|
||||
'id' => $cancellationLog->id,
|
||||
'vehicle' => [
|
||||
'id' => $vehicle->id,
|
||||
'placa' => $vehicle->placa,
|
||||
'niv' => $vehicle->niv,
|
||||
],
|
||||
'old_tag' => [
|
||||
'id' => $tag->id,
|
||||
'folio' => $oldFolio,
|
||||
'tag_number' => $oldTagNumber,
|
||||
'new_status' => 'Cancelado',
|
||||
],
|
||||
'new_tag' => $newTag ? [
|
||||
'id' => $newTag->id,
|
||||
'folio' => $newTag->folio,
|
||||
'tag_number' => $newTag->tag_number,
|
||||
'status' => $newTag->status->name,
|
||||
] : null,
|
||||
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
||||
'cancellation_observations' => $request->cancellation_observations,
|
||||
'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(),
|
||||
'cancelled_by' => Auth::user()->name,
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error('Error en cancelarConstancia: ' . $e->getMessage(), [
|
||||
'record_id' => $recordId ?? null,
|
||||
'cancellation_reason' => $request->cancellation_reason ?? null,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error al cancelar la constancia',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancelarTagNoAsignado(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'folio' => 'required|string|exists:tags,folio',
|
||||
'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id',
|
||||
'cancellation_observations' => 'nullable|string',
|
||||
'module_id' => 'nullable|exists:modules,id',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$tag = Tag::where('folio', $request->folio)->first();
|
||||
|
||||
if (!$tag) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontró el tag con el folio proporcionado.',
|
||||
'folio' => $request->folio,
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que el tag NO esté asignado
|
||||
if ($tag->isAssigned()) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Este tag está asignado a un vehículo. Usa el endpoint de cancelación de constancia.',
|
||||
'tag_status' => $tag->status->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que no esté ya cancelado
|
||||
if ($tag->isCancelled()) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag ya está cancelado.',
|
||||
]);
|
||||
}
|
||||
|
||||
$observations = $request->cancellation_observations;
|
||||
|
||||
// Verificar que existe el motivo de cancelación ANTES de crear el log
|
||||
$cancellationReason = CatalogCancellationReason::find($request->cancellation_reason_id);
|
||||
if (!$cancellationReason) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El motivo de cancelación no existe.',
|
||||
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||
]);
|
||||
}
|
||||
|
||||
$cancellationLog = TagCancellationLog::create([
|
||||
'tag_id' => $tag->id,
|
||||
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||
'cancellation_observations' => $observations,
|
||||
'cancellation_at' => now(),
|
||||
'cancelled_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
// Cargar las relaciones necesarias ANTES de usarlas
|
||||
$cancellationLog->load(['cancellationReason', 'cancelledBy']);
|
||||
|
||||
// Actualizar el módulo del tag si se proporciona
|
||||
if ($request->filled('module_id')) {
|
||||
$tag->module_id = $request->module_id;
|
||||
$tag->save();
|
||||
}
|
||||
|
||||
// Cancelar el tag
|
||||
$tag->markAsDamaged();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Recargar el tag con sus relaciones actualizadas
|
||||
$tag->load(['status', 'cancellationLogs.cancellationReason', 'cancellationLogs.cancelledBy', 'module']);
|
||||
|
||||
// Obtener datos de cancelación del último log
|
||||
$lastCancellation = $tag->cancellationLogs()
|
||||
->with(['cancellationReason', 'cancelledBy'])
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
// Validar que existe el log de cancelación
|
||||
if (!$lastCancellation) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error: No se encontró el log de cancelación después de crearlo.',
|
||||
'tag_id' => $tag->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Preparar datos para el PDF con validaciones defensivas
|
||||
$cancellationData = [
|
||||
'fecha' => $lastCancellation->cancellation_at ? $lastCancellation->cancellation_at->format('d/m/Y') : now()->format('d/m/Y'),
|
||||
'folio' => $tag->folio ?? '',
|
||||
'tag_number' => $tag->tag_number ?? 'N/A',
|
||||
'motivo' => ($lastCancellation->cancellationReason && isset($lastCancellation->cancellationReason->name))
|
||||
? $lastCancellation->cancellationReason->name
|
||||
: 'No especificado',
|
||||
'operador' => ($lastCancellation->cancelledBy && isset($lastCancellation->cancelledBy->name))
|
||||
? $lastCancellation->cancelledBy->name
|
||||
: 'Sistema',
|
||||
'modulo' => ($tag->module && isset($tag->module->name)) ? $tag->module->name : 'No especificado',
|
||||
'ubicacion' => ($tag->module && isset($tag->module->address)) ? $tag->module->address : 'No especificado',
|
||||
];
|
||||
|
||||
try {
|
||||
$pdf = Pdf::loadView('pdfs.tag', [
|
||||
'cancellation' => $cancellationData,
|
||||
])
|
||||
->setPaper('a4', 'portrait')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('constancia_cancelada_' . ($tag->tag_number ?? $tag->folio) . '.pdf');
|
||||
} catch (\Exception $e) {
|
||||
// Retornar error como JSON
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Tag cancelado pero error al generar PDF',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
// Errores de validación
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error de validación',
|
||||
'errors' => $e->errors(),
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al cancelar el tag',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\CancelConstanciaRequest;
|
||||
use App\Models\CatalogCancellationReason;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*/
|
||||
class CatalogController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$type = $request->query('type');
|
||||
|
||||
$query = CatalogCancellationReason::query();
|
||||
|
||||
if ($type === 'cancelacion') {
|
||||
$query->forCancellation();
|
||||
} elseif ($type === 'sustitucion') {
|
||||
$query->forSubstitution();
|
||||
} else {
|
||||
$query->orderBy('id');
|
||||
}
|
||||
|
||||
$reasons = $query->get(['id', 'code', 'name', 'description', 'applies_to']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Razones de cancelación obtenidas exitosamente',
|
||||
'data' => $reasons,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$reason = CatalogCancellationReason::find($id);
|
||||
|
||||
if (!$reason) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Razón de cancelación no encontrada',
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Razón de cancelación obtenida exitosamente',
|
||||
'data' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|unique:catalog_cancellation_reasons,code',
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
'applies_to' => 'required|in:cancelacion,sustitucion,ambos',
|
||||
]);
|
||||
|
||||
$reason = CatalogCancellationReason::create($validated);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Razón de cancelación creada exitosamente',
|
||||
'data' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$reason = CatalogCancellationReason::find($id);
|
||||
|
||||
if (!$reason) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Razón de cancelación no encontrada',
|
||||
]);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
'applies_to' => 'required|in:cancelacion,sustitucion,ambos',
|
||||
]);
|
||||
|
||||
$reason->update($validated);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Razón de cancelación actualizada exitosamente',
|
||||
'data' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$reason = CatalogCancellationReason::find($id);
|
||||
|
||||
if (!$reason) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Razón de cancelación no encontrada',
|
||||
]);
|
||||
}
|
||||
|
||||
$reason->delete();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Razón de cancelación eliminada exitosamente',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Requests\Repuve\CatalogNameImgStoreRequest;
|
||||
use App\Http\Requests\Repuve\CatalogNameImgUpdateRequest;
|
||||
use Notsoweb\LaravelCore\Controllers\VueController;
|
||||
use App\Models\CatalogNameImg;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
class CatalogNameImgController extends VueController
|
||||
{
|
||||
/**
|
||||
* Listar
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$names = CatalogNameImg::orderBy('id', 'ASC')->get();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'names' => $names,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear
|
||||
*/
|
||||
public function store(CatalogNameImgStoreRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$catalogNameImg = CatalogNameImg::create($validated);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'name' => $catalogNameImg,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar
|
||||
*/
|
||||
public function update(CatalogNameImgUpdateRequest $request, $id)
|
||||
{
|
||||
$catalogName = CatalogNameImg::findOrFail($id);
|
||||
|
||||
$validated = $request->validated([
|
||||
'name' => 'required|string|max:255|unique:catalog_name_img,name,' . $id,
|
||||
]);
|
||||
|
||||
$catalogName->update($validated);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'name' => $catalogName,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
$catalogName = CatalogNameImg::findOrFail($id);
|
||||
$catalogName->delete();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Nombre del catálogo eliminado correctamente.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar el nombre del catálogo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,230 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Requests\Repuve\DeviceStoreRequest;
|
||||
use App\Http\Requests\Repuve\DeviceUpdateRequest;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceModule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$devices = Device::query()->with('deviceModules.module', 'deviceModules.user');
|
||||
|
||||
if ($request->filled('serie')) {
|
||||
$devices->where('serie', 'LIKE', '%' . $request->input('serie') . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('brand')) {
|
||||
$devices->where('brand', 'LIKE', '%' . $request->input('brand') . '%');
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'devices' => $devices->paginate(config('app.pagination')),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener la lista de dispositivos.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(DeviceStoreRequest $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Crear el dispositivo
|
||||
$device = Device::create([
|
||||
'brand' => $request->input('brand'),
|
||||
'serie' => $request->input('serie'),
|
||||
'mac_address' => $request->input('mac_address'),
|
||||
'status' => $request->input('status', true),
|
||||
]);
|
||||
|
||||
// Asignar módulo y usuarios usando el modelo DeviceModule
|
||||
$userIds = $request->input('user_id');
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
DeviceModule::create([
|
||||
'device_id' => $device->id,
|
||||
'module_id' => $request->module_id,
|
||||
'user_id' => $userId,
|
||||
'status' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$device->load('modules');
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Dispositivo creado exitosamente.',
|
||||
'device' => [
|
||||
'id' => $device->id,
|
||||
'brand' => $device->brand,
|
||||
'serie' => $device->serie,
|
||||
'status' => $device->status,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al crear el dispositivo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$device = Device::with('deviceModules.module', 'deviceModules.user')->findOrFail($id);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'device' => $device,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener el dispositivo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(DeviceUpdateRequest $request, $id)
|
||||
{
|
||||
try {
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$device = Device::findOrFail($id);
|
||||
|
||||
$device->update($request->only(['brand', 'serie', 'mac_address', 'status']));
|
||||
|
||||
DeviceModule::where('device_id', $device->id)->delete();
|
||||
|
||||
$userIds = $request->input('user_id');
|
||||
foreach ($userIds as $userId) {
|
||||
DeviceModule::create([
|
||||
'device_id' => $device->id,
|
||||
'module_id' => $request->module_id,
|
||||
'user_id' => $userId,
|
||||
'status' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$device->load(['deviceModules.module', 'deviceModules.user']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Dispositivo actualizado exitosamente.',
|
||||
'device' => [
|
||||
'id' => $device->id,
|
||||
'brand' => $device->brand,
|
||||
'serie' => $device->serie,
|
||||
'mac_address' => $device->mac_address,
|
||||
'status' => $device->status,
|
||||
'module' => $device->deviceModules->first()?->module,
|
||||
'authorized_users' => $device->deviceModules
|
||||
->filter(fn($dm) => $dm->user !== null)
|
||||
->map(fn($dm) => [
|
||||
'id' => $dm->user->id,
|
||||
'name' => $dm->user->full_name,
|
||||
'email' => $dm->user->email,
|
||||
])
|
||||
->unique('id')
|
||||
->values(),
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al actualizar el dispositivo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$device = Device::findOrFail($id);
|
||||
$device->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Dispositivo eliminado exitosamente.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar el dispositivo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambiar solo el status de un dispositivo
|
||||
*/
|
||||
public function toggleStatus(int $id)
|
||||
{
|
||||
try {
|
||||
$device = Device::findOrFail($id);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$newStatus = !$device->status;
|
||||
$device->update([
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$device->refresh();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => $device->status
|
||||
? 'Dispositivo activado exitosamente'
|
||||
: 'Dispositivo desactivado exitosamente',
|
||||
'device' => [
|
||||
'id' => $device->id,
|
||||
'brand' => $device->brand,
|
||||
'serie' => $device->serie,
|
||||
'mac_address' => $device->mac_address,
|
||||
'status' => $device->status ? 'Activo' : 'Inactivo',
|
||||
'updated_at' => $device->updated_at->format('Y-m-d H:i:s'),
|
||||
],
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Dispositivo no encontrado',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error al cambiar status del módulo: ' . $e->getMessage(), [
|
||||
'module_id' => $id,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al cambiar status del módulo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,606 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Requests\Repuve\VehicleStoreRequest;
|
||||
use App\Models\CatalogNameImg;
|
||||
use App\Models\Vehicle;
|
||||
use App\Models\Record;
|
||||
use App\Models\Owner;
|
||||
use App\Models\File;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VehicleTagLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Services\RepuveService;
|
||||
use App\Services\PadronEstatalService;
|
||||
use App\Jobs\ProcessRepuveResponse;
|
||||
|
||||
class InscriptionController extends Controller
|
||||
{
|
||||
private RepuveService $repuveService;
|
||||
private PadronEstatalService $padronEstatalService;
|
||||
|
||||
public function __construct(
|
||||
RepuveService $repuveService,
|
||||
PadronEstatalService $padronEstatalService
|
||||
) {
|
||||
$this->repuveService = $repuveService;
|
||||
$this->padronEstatalService = $padronEstatalService;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inscripción de vehículo al REPUVE
|
||||
*/
|
||||
public function vehicleInscription(VehicleStoreRequest $request)
|
||||
{
|
||||
try {
|
||||
$folio = $request->input('folio');
|
||||
$tagNumber = $request->input('tag_number');
|
||||
$placa = $request->input('placa');
|
||||
$telefono = $request->input('telefono');
|
||||
|
||||
// Buscar Tag y validar que NO tenga vehículo asignado
|
||||
$tag = Tag::where('folio', $folio)->first();
|
||||
|
||||
if (!$tag) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontró el tag con el folio y tag_number proporcionados.',
|
||||
'folio' => $folio,
|
||||
'tag_number' => $tagNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$tag->isAvailable()) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag ya está asignado a un vehículo. Use actualizar en su lugar.',
|
||||
'current_status' => $tag->status->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// Iniciar transacción
|
||||
DB::beginTransaction();
|
||||
|
||||
if (!$tag->tag_number) {
|
||||
$existingTag = Tag::where('tag_number', $tagNumber)->first();
|
||||
if ($existingTag && $existingTag->id !== $tag->id) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag_number ya está asignado a otro folio.',
|
||||
'tag_number' => $tagNumber,
|
||||
'folio_existente' => $existingTag->folio,
|
||||
]);
|
||||
}
|
||||
// Guardar tag_number
|
||||
$tag->tag_number = $tagNumber;
|
||||
$tag->save();
|
||||
} elseif ($tag->tag_number !== $tagNumber) {
|
||||
// Si el tag ya tiene un tag_number diferente, validar
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El folio ya tiene un tag_number diferente asignado.',
|
||||
'folio' => $folio,
|
||||
'tag_number_actual' => $tag->tag_number,
|
||||
'tag_number_enviado' => $tagNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
$datosCompletosRaw = $this->padronEstatalService->getVehiculoByPlaca($placa);
|
||||
|
||||
// Obtener datos de API Estatal por placa
|
||||
$vehicleData = $this->padronEstatalService->extraerDatosVehiculo($datosCompletosRaw);
|
||||
$ownerData = $this->padronEstatalService->extraerDatosPropietario($datosCompletosRaw);
|
||||
|
||||
// Obtener NIV de los datos del vehículo para verificar robo
|
||||
$niv = $vehicleData['niv'];
|
||||
|
||||
// Verificar robo (API Repuve Nacional)
|
||||
$resultadoRobo = $this->checkIfStolen($niv, $placa);
|
||||
$isStolen = $resultadoRobo;
|
||||
|
||||
if ($isStolen) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::FORBIDDEN->response([
|
||||
'folio' => $folio,
|
||||
'tag_number' => $tagNumber,
|
||||
'placa' => $placa,
|
||||
'niv' => $niv,
|
||||
'stolen' => true,
|
||||
'detalle_robo' => $resultadoRobo,
|
||||
'message' => 'El vehículo reporta robo. No se puede continuar con la inscripción.',
|
||||
]);
|
||||
}
|
||||
|
||||
// Crear propietario
|
||||
$owner = Owner::updateOrCreate(
|
||||
['rfc' => $ownerData['rfc']],
|
||||
[
|
||||
'name' => $ownerData['name'],
|
||||
'paternal' => $ownerData['paternal'],
|
||||
'maternal' => $ownerData['maternal'],
|
||||
'curp' => $ownerData['curp'],
|
||||
'address' => $ownerData['address'],
|
||||
'tipopers' => $ownerData['tipopers'],
|
||||
'pasaporte' => $ownerData['pasaporte'],
|
||||
'licencia' => $ownerData['licencia'],
|
||||
'ent_fed' => $ownerData['ent_fed'],
|
||||
'munic' => $ownerData['munic'],
|
||||
'callep' => $ownerData['callep'],
|
||||
'num_ext' => $ownerData['num_ext'],
|
||||
'num_int' => $ownerData['num_int'],
|
||||
'colonia' => $ownerData['colonia'],
|
||||
'cp' => $ownerData['cp'],
|
||||
'telefono' => $telefono
|
||||
]
|
||||
);
|
||||
|
||||
// Crear vehículo
|
||||
$vehicle = Vehicle::create([
|
||||
'placa' => $vehicleData['placa'],
|
||||
'niv' => $vehicleData['niv'],
|
||||
'marca' => $vehicleData['marca'],
|
||||
'linea' => $vehicleData['linea'],
|
||||
'sublinea' => $vehicleData['sublinea'],
|
||||
'modelo' => $vehicleData['modelo'],
|
||||
'color' => $vehicleData['color'],
|
||||
'numero_motor' => $vehicleData['numero_motor'],
|
||||
'clase_veh' => $vehicleData['clase_veh'],
|
||||
'tipo_servicio' => $vehicleData['tipo_servicio'],
|
||||
'rfv' => $vehicleData['rfv'],
|
||||
'ofcexpedicion' => $vehicleData['ofcexpedicion'],
|
||||
'fechaexpedicion' => $vehicleData['fechaexpedicion'],
|
||||
'tipo_veh' => $vehicleData['tipo_veh'],
|
||||
'numptas' => $vehicleData['numptas'],
|
||||
'observac' => $vehicleData['observac'],
|
||||
'cve_vehi' => $vehicleData['cve_vehi'],
|
||||
'nrpv' => $vehicleData['nrpv'],
|
||||
'tipo_mov' => $vehicleData['tipo_mov'],
|
||||
'owner_id' => $owner->id,
|
||||
]);
|
||||
|
||||
// Asignar Tag al vehículo
|
||||
$tag->markAsAssigned($vehicle->id, $folio);
|
||||
|
||||
VehicleTagLog::create([
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'tag_id' => $tag->id,
|
||||
'action_type' => 'inscripcion',
|
||||
'performed_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
// Crear registro
|
||||
$record = Record::create([
|
||||
'folio' => $folio,
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'user_id' => Auth::id(),
|
||||
'module_id' => Auth::user()->module_id,
|
||||
]);
|
||||
|
||||
// Procesar archivos
|
||||
$uploadedFiles = [];
|
||||
if ($request->hasFile('files')) {
|
||||
$files = $request->file('files');
|
||||
$nameIds = $request->input('name_id', []);
|
||||
|
||||
if (!empty($nameIds)) {
|
||||
$validIds = CatalogNameImg::whereIn('id', $nameIds)->pluck('id')->toArray();
|
||||
if (count($validIds) !== count($nameIds)) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Algunos IDs del catálogo de nombres no son válidos',
|
||||
'provided_id' => $nameIds,
|
||||
'valid_id' => $validIds,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($files as $index => $file) {
|
||||
// Obtener el name_id del request o usar null como fallback
|
||||
$nameId = $nameIds[$index] ?? null;
|
||||
|
||||
if ($nameId === null) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => "Falta el name_id para el archivo en el índice {$index}",
|
||||
'file_index' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener el nombre del catálogo para el nombre del archivo
|
||||
$catalogName = CatalogNameImg::find($nameId);
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension;
|
||||
$path = $file->storeAs("records/{$record->folio}", $fileName, 'public');
|
||||
$md5 = md5_file($file->getRealPath());
|
||||
|
||||
$fileRecord = File::create([
|
||||
'name_id' => $nameId,
|
||||
'path' => $path,
|
||||
'md5' => $md5,
|
||||
'record_id' => $record->id,
|
||||
]);
|
||||
|
||||
$uploadedFiles[] = [
|
||||
'id' => $fileRecord->id,
|
||||
'name' => $catalogName->name,
|
||||
'path' => $fileRecord->path,
|
||||
'url' => $fileRecord->url,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$record->load(['vehicle.owner', 'vehicle.tag', 'files', 'user']);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'success' => true,
|
||||
'message' => 'Vehículo y propietario guardados exitosamente.',
|
||||
'record' => [
|
||||
'id' => $record->id,
|
||||
'folio' => $record->folio,
|
||||
'vehicle_id' => $vehicle->id,
|
||||
'user_id' => $record->user_id,
|
||||
'created_at' => $record->created_at->toDateTimeString(),
|
||||
],
|
||||
'vehicle' => [
|
||||
'id' => $record->vehicle->id,
|
||||
'placa' => $record->vehicle->placa,
|
||||
'niv' => $record->vehicle->niv,
|
||||
'marca' => $record->vehicle->marca,
|
||||
'linea' => $record->vehicle->linea,
|
||||
'modelo' => $record->vehicle->modelo,
|
||||
'color' => $record->vehicle->color,
|
||||
],
|
||||
'owner' => [
|
||||
'id' => $record->vehicle->owner->id,
|
||||
'full_name' => $record->vehicle->owner->full_name,
|
||||
'rfc' => $record->vehicle->owner->rfc,
|
||||
],
|
||||
'tag' => [
|
||||
'id' => $record->vehicle->tag->id,
|
||||
'folio' => $record->vehicle->tag->folio,
|
||||
'tag_number' => $record->vehicle->tag->tag_number,
|
||||
'status' => $record->vehicle->tag->status->name,
|
||||
],
|
||||
'files' => $uploadedFiles,
|
||||
'total_files' => count($uploadedFiles),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error al procesar la inscripción del vehículo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkIfStolen(?string $niv = null, ?string $placa = null)
|
||||
{
|
||||
return $this->repuveService->verificarRobo($niv, $placa);
|
||||
}
|
||||
|
||||
public function searchRecord(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'folio' => 'nullable|string',
|
||||
'placa' => 'nullable|string',
|
||||
'vin' => 'nullable|string',
|
||||
'tag_number' => 'nullable|string',
|
||||
'module_id' => 'nullable|integer|exists:modules,id',
|
||||
'action_type' => 'nullable|string|in:inscripcion,actualizacion,sustitucion,cancelacion',
|
||||
'status' => 'nullable|string',
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
], [
|
||||
'folio.required_without_all' => 'Se requiere al menos un criterio de búsqueda.',
|
||||
'placa.required_without_all' => 'Se requiere al menos un criterio de búsqueda.',
|
||||
'vin.required_without_all' => 'Se requiere al menos un criterio de búsqueda.',
|
||||
'start_date.date' => 'La fecha de inicio debe ser una fecha válida.',
|
||||
'end_date.date' => 'La fecha de fin debe ser una fecha válida.',
|
||||
'end_date.after_or_equal' => 'La fecha de fin debe ser posterior o igual a la fecha de inicio.',
|
||||
]);
|
||||
|
||||
$records = Record::with([
|
||||
// Vehículo y propietario
|
||||
'vehicle',
|
||||
'vehicle.owner',
|
||||
|
||||
// Tag con Package
|
||||
'vehicle.tag:id,vehicle_id,folio,tag_number,status_id,package_id',
|
||||
'vehicle.tag.status:id,code,name',
|
||||
'vehicle.tag.package:id,lot,box_number',
|
||||
|
||||
// Archivos
|
||||
'files:id,record_id,name_id,path,md5',
|
||||
'files.catalogName:id,name',
|
||||
|
||||
// Operador y módulo
|
||||
'user:id,name,email,module_id',
|
||||
'module:id,name',
|
||||
|
||||
// Error si existe
|
||||
'error:id,code,description',
|
||||
|
||||
// Log de acciones
|
||||
'vehicle.vehicleTagLogs' => function ($q) {
|
||||
$q->with([
|
||||
'tag:id,folio,tag_number,status_id,module_id,package_id',
|
||||
'tag.status:id,code,name',
|
||||
'tag.module:id,name',
|
||||
'tag.package:id,lot,box_number'
|
||||
])->orderBy('created_at', 'DESC');
|
||||
},
|
||||
])->orderBy('id', 'ASC');
|
||||
|
||||
if ($request->filled('folio')) {
|
||||
$records->whereHas('vehicle.tag', function ($q) use ($request) {
|
||||
$q->where('folio', 'LIKE', '%' . $request->input('folio') . '%');
|
||||
});
|
||||
}
|
||||
if ($request->filled('placa')) {
|
||||
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||
$q->where('placa', 'LIKE', '%' . $request->input('placa') . '%');
|
||||
});
|
||||
}
|
||||
if ($request->filled('vin')) {
|
||||
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||
$q->where('niv', 'LIKE', '%' . $request->input('vin') . '%');
|
||||
});
|
||||
}
|
||||
if ($request->filled('tag_number')) {
|
||||
$records->whereHas('vehicle.tag', function ($q) use ($request) {
|
||||
$q->where('tag_number', 'LIKE', '%' . $request->input('tag_number') . '%');
|
||||
});
|
||||
}
|
||||
// Filtro por módulo
|
||||
if ($request->filled('module_id')) {
|
||||
$records->where('module_id', $request->input('module_id'));
|
||||
}
|
||||
|
||||
// Filtro por tipo de acción
|
||||
if ($request->filled('action_type')) {
|
||||
$records->whereHas('vehicle.vehicleTagLogs', function ($q) use ($request) {
|
||||
$q->where('action_type', $request->input('action_type'))
|
||||
->whereRaw('id = (
|
||||
SELECT MAX(id)
|
||||
FROM vehicle_tags_logs
|
||||
WHERE vehicle_id = vehicle.id
|
||||
)');
|
||||
});
|
||||
}
|
||||
|
||||
// Filtro por status del tag
|
||||
if ($request->filled('status')) {
|
||||
$records->whereHas('vehicle.tag.status', function ($q) use ($request) {
|
||||
$q->where('code', $request->input('status'));
|
||||
});
|
||||
}
|
||||
|
||||
// Filtro por rango de fechas
|
||||
if ($request->filled('start_date')) {
|
||||
$records->whereDate('created_at', '>=', $request->input('start_date'));
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$records->whereDate('created_at', '<=', $request->input('end_date'));
|
||||
}
|
||||
|
||||
// Paginación
|
||||
$paginatedRecords = $records->paginate(config('app.pagination'));
|
||||
|
||||
if ($paginatedRecords->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron registros con los criterios de búsqueda proporcionados.',
|
||||
]);
|
||||
}
|
||||
|
||||
// Transformación de datos
|
||||
$paginatedRecords->getCollection()->transform(function ($record) {
|
||||
$latestLog = $record->vehicle->vehicleTagLogs->first();
|
||||
|
||||
// Construir historial completo de tags
|
||||
$tagsHistory = [];
|
||||
$order = 1;
|
||||
$vehicleLogs = $record->vehicle->vehicleTagLogs->sortBy('created_at');
|
||||
$processedTags = [];
|
||||
|
||||
foreach ($vehicleLogs as $log) {
|
||||
$tagId = $log->tag_id;
|
||||
if ($tagId && !in_array($tagId, $processedTags)) {
|
||||
$processedTags[] = $tagId;
|
||||
$tag = $log->tag;
|
||||
|
||||
// Buscar fecha de cancelación si existe
|
||||
$cancelLog = $vehicleLogs
|
||||
->where('tag_id', $tagId)
|
||||
->whereIn('action_type', ['cancelacion', 'sustitucion'])
|
||||
->whereNotNull('cancellation_at')
|
||||
->first();
|
||||
|
||||
$tagsHistory[] = [
|
||||
'order' => $order++,
|
||||
'tag_id' => $tagId,
|
||||
'folio' => $tag?->folio,
|
||||
'tag_number' => $tag?->tag_number,
|
||||
'status' => $tag?->status?->code ?? 'unknown',
|
||||
'module_name' => $tag?->module?->name,
|
||||
'box_number' => $tag?->package?->box_number,
|
||||
'assigned_at' => $vehicleLogs->where('tag_id', $tagId)
|
||||
->whereIn('action_type', ['inscripcion', 'sustitucion'])
|
||||
->first()?->created_at,
|
||||
'cancelled_at' => $cancelLog?->cancellation_at,
|
||||
'is_current' => $tag?->id === $record->vehicle->tag?->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $record->id,
|
||||
'folio' => $record->folio,
|
||||
'created_at' => $record->created_at,
|
||||
|
||||
// TIPO DE TRÁMITE
|
||||
'action_type' => $latestLog?->action_type ?? 'inscripcion',
|
||||
'action_date' => $latestLog?->created_at ?? $record->created_at,
|
||||
|
||||
// HISTORIAL DE TAGS
|
||||
'tags_history' => $tagsHistory,
|
||||
'total_tags' => count($tagsHistory),
|
||||
|
||||
// MÓDULO
|
||||
'module' => $record->module ? [
|
||||
'id' => $record->module->id,
|
||||
'name' => $record->module->name,
|
||||
] : null,
|
||||
|
||||
// OPERADOR
|
||||
'operator' => $record->user ? [
|
||||
'id' => $record->user->id,
|
||||
'name' => $record->user->name,
|
||||
'email' => $record->user->email,
|
||||
] : null,
|
||||
|
||||
// VEHÍCULO
|
||||
'vehicle' => [
|
||||
'id' => $record->vehicle->id,
|
||||
'placa' => $record->vehicle->placa,
|
||||
'niv' => $record->vehicle->niv,
|
||||
'marca' => $record->vehicle->marca,
|
||||
'linea' => $record->vehicle->linea,
|
||||
'sublinea' => $record->vehicle->sublinea,
|
||||
'modelo' => $record->vehicle->modelo,
|
||||
'color' => $record->vehicle->color,
|
||||
'numero_motor' => $record->vehicle->numero_motor,
|
||||
'clase_veh' => $record->vehicle->clase_veh,
|
||||
'tipo_servicio' => $record->vehicle->tipo_servicio,
|
||||
'rfv' => $record->vehicle->rfv,
|
||||
'nrpv' => $record->vehicle->nrpv,
|
||||
'reporte_robo' => $record->vehicle->reporte_robo,
|
||||
|
||||
// PROPIETARIO
|
||||
'owner' => $record->vehicle->owner ? [
|
||||
'id' => $record->vehicle->owner->id,
|
||||
'name' => $record->vehicle->owner->name,
|
||||
'paternal' => $record->vehicle->owner->paternal,
|
||||
'maternal' => $record->vehicle->owner->maternal,
|
||||
'full_name' => $record->vehicle->owner->full_name,
|
||||
'rfc' => $record->vehicle->owner->rfc,
|
||||
'curp' => $record->vehicle->owner->curp,
|
||||
'telefono' => $record->vehicle->owner->telefono,
|
||||
'address' => $record->vehicle->owner->address,
|
||||
] : null,
|
||||
|
||||
// TAG ACTUAL
|
||||
'tag' => $record->vehicle->tag ? [
|
||||
'id' => $record->vehicle->tag->id,
|
||||
'folio' => $record->vehicle->tag->folio,
|
||||
'tag_number' => $record->vehicle->tag->tag_number,
|
||||
'status' => $record->vehicle->tag->status ? [
|
||||
'id' => $record->vehicle->tag->status->id,
|
||||
'code' => $record->vehicle->tag->status->code,
|
||||
'name' => $record->vehicle->tag->status->name,
|
||||
] : null,
|
||||
'package' => $record->vehicle->tag->package ? [
|
||||
'id' => $record->vehicle->tag->package->id,
|
||||
'lot' => $record->vehicle->tag->package->lot,
|
||||
'box_number' => $record->vehicle->tag->package->box_number,
|
||||
] : null,
|
||||
] : null,
|
||||
],
|
||||
|
||||
// Archivos
|
||||
'files' => $record->files->map(function ($file) {
|
||||
return [
|
||||
'id' => $file->id,
|
||||
'name_id' => $file->name_id,
|
||||
'name' => $file->catalogName?->name,
|
||||
'path' => $file->path,
|
||||
'url' => $file->url,
|
||||
];
|
||||
}),
|
||||
|
||||
// Error
|
||||
'error' => $record->error ? [
|
||||
'id' => $record->error->id,
|
||||
'code' => $record->error->code,
|
||||
'description' => $record->error->description,
|
||||
] : null,
|
||||
|
||||
// Respuesta de REPUVE
|
||||
'api_response' => $record->api_response,
|
||||
];
|
||||
});
|
||||
return ApiResponse::OK->response([
|
||||
'records' => $paginatedRecords
|
||||
]);
|
||||
}
|
||||
|
||||
public function stolen(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'vin' => 'nullable|string|min:17|max:17',
|
||||
'placa' => 'nullable|string',
|
||||
], [
|
||||
'vin.required_without' => 'Debe proporcionar al menos VIN o PLACA.',
|
||||
'placa.required_without' => 'Debe proporcionar al menos VIN o PLACA.',
|
||||
]);
|
||||
|
||||
// Validar que al menos uno esté presente
|
||||
if (!$request->filled('vin') && !$request->filled('placa')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Debe proporcionar al menos VIN o PLACA.',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$vin = $request->input('vin');
|
||||
$placa = $request->input('placa');
|
||||
|
||||
// Verificar robo usando el servicio
|
||||
$resultado = $this->repuveService->verificarRobo($vin, $placa);
|
||||
$isStolen = $resultado['is_robado'] ?? false;
|
||||
|
||||
$vehicle = Vehicle::where(function ($query) use ($vin, $placa) {
|
||||
if ($vin) {
|
||||
$query->orWhere('niv', $vin);
|
||||
}
|
||||
if ($placa) {
|
||||
$query->orWhere('placa', $placa);
|
||||
}
|
||||
})->first();
|
||||
|
||||
$actualizar = false;
|
||||
if ($vehicle) {
|
||||
$vehicle->reporte_robo = $isStolen;
|
||||
$vehicle->save();
|
||||
$actualizar = true;
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'vin' => $vin ?: null,
|
||||
'placa' => $placa ?: null,
|
||||
'robado' => $isStolen,
|
||||
'estatus' => $isStolen ? 'REPORTADO COMO ROBADO' : 'SIN REPORTE DE ROBO',
|
||||
'message' => $isStolen
|
||||
? 'El vehículo tiene reporte de robo en REPUVE.'
|
||||
: 'El vehículo no tiene reporte de robo.',
|
||||
'fecha' => now()->toDateTimeString(),
|
||||
'detalle_robo' => $resultado,
|
||||
'existe_registro_BD' => $vehicle ? true : false,
|
||||
'actualizado_reporte_robo' => $actualizar,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al consultar el estado de robo del vehículo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,258 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\ModuleStoreRequest;
|
||||
use App\Http\Requests\Repuve\ModuleUpdateRequest;
|
||||
use App\Models\Module;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
class ModuleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Listar módulos existentes
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$modules = Module::with([
|
||||
'responsible:id,name,email',
|
||||
'municipality:id,code,name',
|
||||
'users:id,name,paternal,maternal,email,module_id',
|
||||
'users.roles:id,name,description'
|
||||
]);
|
||||
|
||||
// Filtro por nombre
|
||||
if ($request->filled('name')) {
|
||||
$modules->where('name', 'like', '%' . $request->input('name') . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('municipality')) {
|
||||
$modules->whereHas('municipality', function ($q) use ($request) {
|
||||
$q->where('name', 'like', '%' . $request->input('municipality') . '%');
|
||||
});
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'modules' => $modules->paginate(config('app.pagination')),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al listar módulos',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear un nuevo módulo
|
||||
*/
|
||||
public function store(ModuleStoreRequest $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Crear el módulo
|
||||
$module = Module::create([
|
||||
'name' => $request->input('name'),
|
||||
'responsible_id' => $request->input('responsible_id'),
|
||||
'municipality_id' => $request->input('municipality_id'),
|
||||
'address' => $request->input('address'),
|
||||
'colony' => $request->input('colony'),
|
||||
'cp' => $request->input('cp'),
|
||||
'longitude' => $request->input('longitude'),
|
||||
'latitude' => $request->input('latitude'),
|
||||
'status' => $request->input('status', true), // Por defecto activo
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$module->load('municipality');
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Módulo creado exitosamente',
|
||||
'module' => [
|
||||
'name' => $module->name,
|
||||
'responsible_id' => $module->responsible_id,
|
||||
'municipality' => $module->municipality ? [
|
||||
'id' => $module->municipality->id,
|
||||
'code' => $module->municipality->code,
|
||||
'name' => $module->municipality->name,
|
||||
] : null,
|
||||
'address' => $module->address,
|
||||
'colony' => $module->colony,
|
||||
'cp' => $module->cp,
|
||||
'longitude' => $module->longitude,
|
||||
'latitude' => $module->latitude,
|
||||
'status' => $module->status ? 'Activo' : 'Inactivo',
|
||||
'created_at' => $module->created_at->format('Y-m-d H:i:s'),
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al crear módulo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$modules = Module::with([
|
||||
'responsible:id,name,email',
|
||||
'municipality:id,code,name',
|
||||
'users:id,name,paternal,maternal,email,module_id',
|
||||
'users.roles:id,name,description'
|
||||
])->find($id);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'module' => $modules,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener el módulo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar un módulo existente
|
||||
*/
|
||||
public function update(ModuleUpdateRequest $request, int $id)
|
||||
{
|
||||
try {
|
||||
$module = Module::findOrFail($id);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Actualizar solo los campos que vienen en el request
|
||||
$module->update($request->validated());
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Cargar la relación actualizada
|
||||
$module->load('municipality');
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Módulo actualizado exitosamente',
|
||||
'module' => [
|
||||
'name' => $module->name,
|
||||
'responsible_id' => $module->responsible_id,
|
||||
'municipality' => $module->municipality ? [
|
||||
'id' => $module->municipality->id,
|
||||
'code' => $module->municipality->code,
|
||||
'name' => $module->municipality->name,
|
||||
] : null,
|
||||
'address' => $module->address,
|
||||
'colony' => $module->colony,
|
||||
'cp' => $module->cp,
|
||||
'longitude' => $module->longitude,
|
||||
'latitude' => $module->latitude,
|
||||
'status' => $module->status ? 'Activo' : 'Inactivo',
|
||||
'updated_at' => $module->updated_at->format('Y-m-d H:i:s'),
|
||||
],
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Módulo no encontrado',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al actualizar módulo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
try {
|
||||
$module = Module::findOrFail($id);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$module->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Módulo eliminado exitosamente',
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Módulo no encontrado',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error al eliminar módulo: ' . $e->getMessage(), [
|
||||
'module_id' => $id,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar el módulo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambiar solo el status de un módulo
|
||||
*/
|
||||
public function toggleStatus(int $id)
|
||||
{
|
||||
try {
|
||||
$module = Module::findOrFail($id);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$newStatus = !$module->status;
|
||||
|
||||
$module->update([
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$module->refresh();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => $module->status
|
||||
? 'Módulo activado exitosamente'
|
||||
: 'Módulo desactivado exitosamente',
|
||||
'module' => [
|
||||
'id' => $module->id,
|
||||
'name' => $module->name,
|
||||
'status' => $module->status,
|
||||
'status_text' => $module->status ? 'Activo' : 'Inactivo',
|
||||
'updated_at' => $module->updated_at->format('Y-m-d H:i:s'),
|
||||
],
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Módulo no encontrado',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error al cambiar status del módulo: ' . $e->getMessage(), [
|
||||
'module_id' => $id,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al cambiar status del módulo',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
<?php namespace App\Http\Controllers\Repuve;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Models\Municipality;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class MunicipalityController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$municipalities = Municipality::orderBy('id', 'ASC')->get();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $municipalities,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|unique:municipalities,code',
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
$municipality = Municipality::create([
|
||||
'code' => $request->input('code'),
|
||||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'data' => $municipality,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$municipality = Municipality::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'code' => 'required|unique:municipalities,code,' . $municipality->id,
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
$municipality->update([
|
||||
'code' => $request->input('code'),
|
||||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $municipality,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$municipality = Municipality::findOrFail($id);
|
||||
$municipality->delete();
|
||||
|
||||
return ApiResponse::NO_CONTENT->response();
|
||||
}
|
||||
}
|
||||
@ -1,485 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\PackageStoreRequest;
|
||||
use App\Http\Requests\Repuve\PackageUpdateRequest;
|
||||
use App\Models\CatalogTagStatus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Database\QueryException;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tag;
|
||||
|
||||
class PackageController extends Controller
|
||||
{
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
// Si NO hay filtro de caja, no cargar las relaciones de tags para optimizar
|
||||
$shouldLoadTags = $request->filled('caja') || $request->filled('box_number');
|
||||
|
||||
$packages = Package::query();
|
||||
|
||||
if ($shouldLoadTags) {
|
||||
$packages->with([
|
||||
'tags:id,folio,tag_number,package_id,status_id,vehicle_id,module_id',
|
||||
'tags.status:id,code,name',
|
||||
'tags.vehicle:id,placa,niv',
|
||||
'tags.module:id,name',
|
||||
'user:id,name,email'
|
||||
]);
|
||||
} else {
|
||||
$packages->with('user:id,name,email');
|
||||
}
|
||||
|
||||
$packages->withCount('tags')->orderBy('id', 'ASC');
|
||||
|
||||
if ($request->filled('lote') || $request->filled('lot')) {
|
||||
$loteValue = $request->input('lote') ?? $request->input('lot');
|
||||
$packages->where('lot', 'LIKE', '%' . trim($loteValue) . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('caja') || $request->filled('box_number')) {
|
||||
$cajaValue = $request->input('caja') ?? $request->input('box_number');
|
||||
$packages->where('box_number', 'LIKE', '%' . trim($cajaValue) . '%');
|
||||
}
|
||||
|
||||
$paginatedPackages = $packages->paginate(config('app.pagination'));
|
||||
|
||||
// Validación si no hay resultados
|
||||
if ($paginatedPackages->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron paquetes con los criterios de búsqueda proporcionados.',
|
||||
'filters_applied' => [
|
||||
'lote' => $request->input('lote') ?? $request->input('lot'),
|
||||
'caja' => $request->input('caja') ?? $request->input('box_number'),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// Si hay filtro de caja, incluir estadísticas de tags
|
||||
if ($request->filled('caja') || $request->filled('box_number')) {
|
||||
$paginatedPackages->getCollection()->transform(function ($package) {
|
||||
return [
|
||||
'id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
'starting_page' => $package->starting_page,
|
||||
'ending_page' => $package->ending_page,
|
||||
'created_by' => $package->user ? [
|
||||
'id' => $package->user->id,
|
||||
'name' => $package->user->name,
|
||||
'email' => $package->user->email,
|
||||
] : null,
|
||||
'estadisticas' => [
|
||||
'total' => $package->tags->count(),
|
||||
'available' => $package->tags->filter(function($tag) {
|
||||
return $tag->status && $tag->status->code === 'available';
|
||||
})->count(),
|
||||
'assigned' => $package->tags->filter(function($tag) {
|
||||
return $tag->status && $tag->status->code === 'assigned';
|
||||
})->count(),
|
||||
'cancelled' => $package->tags->filter(function($tag) {
|
||||
return $tag->status && $tag->status->code === 'cancelled';
|
||||
})->count(),
|
||||
],
|
||||
'tags' => $package->tags->map(function ($tag) {
|
||||
return [
|
||||
'id' => $tag->id,
|
||||
'folio' => $tag->folio,
|
||||
'tag_number' => $tag->tag_number,
|
||||
'status' => $tag->status ? [
|
||||
'id' => $tag->status->id,
|
||||
'code' => $tag->status->code,
|
||||
'name' => $tag->status->name,
|
||||
] : null,
|
||||
'vehicle' => $tag->vehicle ? [
|
||||
'id' => $tag->vehicle->id,
|
||||
'placa' => $tag->vehicle->placa,
|
||||
'niv' => $tag->vehicle->niv,
|
||||
] : null,
|
||||
'module' => $tag->module ? [
|
||||
'id' => $tag->module->id,
|
||||
'name' => $tag->module->name,
|
||||
] : null,
|
||||
];
|
||||
}),
|
||||
];
|
||||
});
|
||||
}
|
||||
return ApiResponse::OK->response([
|
||||
'Paquetes' => $paginatedPackages,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener los paquetes',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function store(PackageStoreRequest $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$package = Package::create([
|
||||
'lot' => $request->lot,
|
||||
'box_number' => $request->box_number,
|
||||
'starting_page' => $request->starting_page,
|
||||
'ending_page' => $request->ending_page,
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
|
||||
// Obtener el status "available" para los tags
|
||||
$statusAvailable = CatalogTagStatus::where('code', 'available')->first();
|
||||
|
||||
if (!$statusAvailable) {
|
||||
throw new \Exception('No se encontró el status "Disponible" para los tags');
|
||||
}
|
||||
|
||||
$existingTags = Tag::whereHas('package', function ($query) use ($request) {
|
||||
$query->where('box_number', $request->box_number);
|
||||
})
|
||||
->whereBetween('folio', [$request->starting_page, $request->ending_page])
|
||||
->get(['folio', 'package_id']);
|
||||
|
||||
if ($existingTags->isNotEmpty()) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Ya existen tags en esta caja con folios en el rango especificado.',
|
||||
'box_number' => $request->box_number,
|
||||
'starting_page' => $request->starting_page,
|
||||
'ending_page' => $request->ending_page,
|
||||
'folios_conflictivos' => $existingTags->pluck('folio')->toArray(),
|
||||
'total_folios_conflictivos' => $existingTags->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Crear los tags según el rango de páginas
|
||||
for ($page = $request->starting_page; $page <= $request->ending_page; $page++) {
|
||||
Tag::create([
|
||||
'folio' => $page,
|
||||
'tag_number' => null,
|
||||
'package_id' => $package->id,
|
||||
'status_id' => $statusAvailable->id,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Paquete registrado exitosamente con sus tags',
|
||||
'package' => $package->load('tags'),
|
||||
'tags_created' => $package->tags()->count(),
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
|
||||
if ($e->getCode() == 23000 && str_contains($e->getMessage(), 'packages_lot_box_unique')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => "Ya existe un paquete con el lote '{$request->lot}' y caja número '{$request->box_number}'.",
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al crear el paquete',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al crear el paquete',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$package = Package::with(['tags'])->findOrFail($id);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'package' => $package,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener el paquete',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(PackageUpdateRequest $request, $id)
|
||||
{
|
||||
try {
|
||||
$package = Package::with('tags')->findOrFail($id);
|
||||
$validated = $request->validated();
|
||||
|
||||
// Validar si el paquete tiene tags asignados
|
||||
$hasTags = $package->tags()->count() > 0;
|
||||
|
||||
// Si tiene tags, validar que no se cambien los rangos de páginas
|
||||
if ($hasTags) {
|
||||
if (isset($validated['starting_page']) && $validated['starting_page'] != $package->starting_page) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se puede cambiar el rango inicial porque el paquete ya tiene tags asignados.',
|
||||
'current_starting_page' => $package->starting_page,
|
||||
'requested_starting_page' => $validated['starting_page'],
|
||||
'tags_count' => $package->tags()->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($validated['ending_page']) && $validated['ending_page'] != $package->ending_page) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se puede cambiar el rango final porque el paquete ya tiene tags asignados.',
|
||||
'current_ending_page' => $package->ending_page,
|
||||
'requested_ending_page' => $validated['ending_page'],
|
||||
'tags_count' => $package->tags()->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Validar que la combinación de lote + caja sea única
|
||||
if (isset($validated['lot']) || isset($validated['box_number'])) {
|
||||
$newLot = $validated['lot'] ?? $package->lot;
|
||||
$newBoxNumber = $validated['box_number'] ?? $package->box_number;
|
||||
|
||||
$existingPackage = Package::where('lot', $newLot)
|
||||
->where('box_number', $newBoxNumber)
|
||||
->where('id', '!=', $package->id)
|
||||
->first();
|
||||
|
||||
if ($existingPackage) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => "Ya existe otro paquete con el lote '{$newLot}' y caja '{$newBoxNumber}'.",
|
||||
'existing_package_id' => $existingPackage->id,
|
||||
'current_lot' => $package->lot,
|
||||
'current_box_number' => $package->box_number,
|
||||
'requested_lot' => $newLot,
|
||||
'requested_box_number' => $newBoxNumber,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Guardar valores anteriores para el log
|
||||
$changes = [];
|
||||
foreach ($validated as $key => $value) {
|
||||
if ($package->$key != $value) {
|
||||
$changes[$key] = [
|
||||
'old' => $package->$key,
|
||||
'new' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay cambios
|
||||
if (empty($changes)) {
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'No se realizaron cambios en el paquete.',
|
||||
'package' => [
|
||||
'id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
'starting_page' => $package->starting_page,
|
||||
'ending_page' => $package->ending_page,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$package->update($validated);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Paquete actualizado exitosamente',
|
||||
'package' => [
|
||||
'id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
'starting_page' => $package->starting_page,
|
||||
'ending_page' => $package->ending_page,
|
||||
'updated_at' => $package->updated_at->format('Y-m-d H:i:s'),
|
||||
],
|
||||
'changes' => $changes,
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
|
||||
if ($e->getCode() == 23000 && str_contains($e->getMessage(), 'packages_lot_box_unique')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Ya existe un paquete con esa combinación de lote y caja.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error de base de datos al actualizar el paquete',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al actualizar el paquete',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$package = Package::findOrFail($id);
|
||||
|
||||
if ($package->tags()->count() > 0) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se puede eliminar el paquete porque tiene tags asociados.',
|
||||
'tags_count' => $package->tags()->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
$package->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Paquete eliminado exitosamente.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar el paquete.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener tags de una caja específica con paginación
|
||||
*/
|
||||
public function getBoxTags(Request $request)
|
||||
{
|
||||
try {
|
||||
// Si no se envían parámetros, obtener el primer paquete
|
||||
if (!$request->has('lot') && !$request->has('box_number')) {
|
||||
$package = Package::with('user')->orderBy('id', 'DESC')->first();
|
||||
|
||||
if (!$package) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron paquetes en el sistema.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Validar parámetros si se proporcionan
|
||||
$validated = $request->validate([
|
||||
'lot' => 'required|string',
|
||||
'box_number' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$lot = $validated['lot'];
|
||||
$boxNumber = $validated['box_number'];
|
||||
|
||||
// Buscar el paquete
|
||||
$package = Package::with('user')->where('lot', $lot)
|
||||
->where('box_number', $boxNumber)
|
||||
->first();
|
||||
|
||||
if (!$package) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontró un paquete con el lote y caja especificados.',
|
||||
'lot' => $lot,
|
||||
'box_number' => $boxNumber,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener tags con paginación
|
||||
$tags = Tag::with(['status:id,code,name', 'vehicle:id,placa,niv', 'module:id,name'])
|
||||
->where('package_id', $package->id)
|
||||
->orderBy('folio', 'ASC');
|
||||
|
||||
// Filtro adicional por status si se proporciona
|
||||
if ($request->filled('status')) {
|
||||
$tags->whereHas('status', function ($q) use ($request) {
|
||||
$q->where('code', $request->input('status'));
|
||||
});
|
||||
}
|
||||
|
||||
if($request->filled('module_id')) {
|
||||
$tags->where('module_id', $request->input('module_id'));
|
||||
}
|
||||
|
||||
$paginatedTags = $tags->paginate($request->input('per_page', 25));
|
||||
|
||||
// Estadísticas generales
|
||||
$estadisticas = [
|
||||
'total' => Tag::where('package_id', $package->id)->count(),
|
||||
'available' => Tag::where('package_id', $package->id)
|
||||
->whereHas('status', fn($q) => $q->where('code', 'available'))
|
||||
->count(),
|
||||
'assigned' => Tag::where('package_id', $package->id)
|
||||
->whereHas('status', fn($q) => $q->where('code', 'assigned'))
|
||||
->count(),
|
||||
'cancelled' => Tag::where('package_id', $package->id)
|
||||
->whereHas('status', fn($q) => $q->where('code', 'cancelled'))
|
||||
->count(),
|
||||
];
|
||||
|
||||
// Transformar tags
|
||||
$paginatedTags->getCollection()->transform(function ($tag) {
|
||||
return [
|
||||
'id' => $tag->id,
|
||||
'folio' => $tag->folio,
|
||||
'tag_number' => $tag->tag_number,
|
||||
'status' => [
|
||||
'id' => $tag->status->id,
|
||||
'code' => $tag->status->code,
|
||||
'name' => $tag->status->name,
|
||||
],
|
||||
'vehicle' => $tag->vehicle ? [
|
||||
'id' => $tag->vehicle->id,
|
||||
'placa' => $tag->vehicle->placa,
|
||||
'niv' => $tag->vehicle->niv,
|
||||
] : null,
|
||||
'module' => $tag->module ? [
|
||||
'id' => $tag->module->id,
|
||||
'name' => $tag->module->name,
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'package' => [
|
||||
'id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
'starting_page' => $package->starting_page,
|
||||
'ending_page' => $package->ending_page,
|
||||
'created_by' => $package->user ? [
|
||||
'id' => $package->user->id,
|
||||
'name' => $package->user->name,
|
||||
'email' => $package->user->email,
|
||||
] : null,
|
||||
],
|
||||
'estadisticas' => $estadisticas,
|
||||
'tags' => $paginatedTags,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener los tags de la caja.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,563 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use App\Models\Record;
|
||||
use App\Models\Tag;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Codedge\Fpdf\Fpdf\Fpdf;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RecordController extends Controller
|
||||
{
|
||||
public function generatePdf($id)
|
||||
{
|
||||
$record = Record::with('vehicle.owner', 'user', 'module')->findOrFail($id);
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.record', compact('record'))
|
||||
->setPaper('a4', 'portrait')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('constancia-inscripcion-' . $id . '.pdf');
|
||||
}
|
||||
|
||||
public function generatePdfVerification($id)
|
||||
{
|
||||
$record = Record::with('vehicle.owner', 'user')->findOrFail($id);
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.verification', compact('record'))
|
||||
->setPaper('a4', 'landscape')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('hoja-verificacion-' . $id . '.pdf');
|
||||
}
|
||||
|
||||
public function generatePdfConstancia($id)
|
||||
{
|
||||
$record = Record::with('vehicle.owner.municipality', 'user')->findOrFail($id);
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.constancia', compact('record'))
|
||||
->setPaper('a4', 'landscape')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('constancia-inscripcion' . $id . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar PDF con las imágenes
|
||||
*/
|
||||
public function generatePdfImages($id)
|
||||
{
|
||||
try {
|
||||
// Obtener el record con sus archivos
|
||||
$record = Record::with(['vehicle.owner', 'files'])->findOrFail($id);
|
||||
|
||||
// Validar que tenga archivos
|
||||
if ($record->files->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El expediente no tiene imágenes adjuntas.',
|
||||
'record_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Crear instancia de FPDF
|
||||
$pdf = new Fpdf('P', 'mm', 'A4');
|
||||
$pdf->SetAutoPageBreak(false);
|
||||
$pdf->SetMargins(10, 10, 10);
|
||||
|
||||
$currentImage = 0;
|
||||
|
||||
foreach ($record->files as $file) {
|
||||
$currentImage++;
|
||||
|
||||
// Buscar archivo en disk 'records'
|
||||
$diskRecords = Storage::disk('records');
|
||||
|
||||
$fileContent = null;
|
||||
if ($diskRecords->exists($file->path)) {
|
||||
$fileContent = $diskRecords->get($file->path);
|
||||
}
|
||||
|
||||
// Si no se encontró el archivo, continuar
|
||||
if ($fileContent === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Agregar nueva página
|
||||
$pdf->AddPage();
|
||||
|
||||
// Header con folio
|
||||
$pdf->SetFillColor(44, 62, 80);
|
||||
$pdf->Rect(0, 0, 210, 20, 'F');
|
||||
$pdf->SetTextColor(255, 255, 255);
|
||||
$pdf->SetFont('Arial', 'B', 14);
|
||||
$pdf->SetXY(10, 7);
|
||||
$pdf->Cell(0, 6, 'FOLIO: ' . $record->folio, 0, 1, 'L');
|
||||
|
||||
// Obtener ruta temporal del archivo
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'pdf_img_');
|
||||
file_put_contents($tempPath, $fileContent);
|
||||
|
||||
// Obtener dimensiones de la imagen
|
||||
$imageInfo = getimagesize($tempPath);
|
||||
|
||||
if ($imageInfo !== false) {
|
||||
list($originalWidth, $originalHeight) = $imageInfo;
|
||||
$imageType = $imageInfo[2];
|
||||
|
||||
$availableWidth = 190; // 210mm - 20mm márgenes
|
||||
$availableHeight = 247; // 297mm - 20mm header - 20mm footer - 10mm márgenes
|
||||
|
||||
// Calcular dimensiones manteniendo proporción
|
||||
$ratio = min($availableWidth / $originalWidth, $availableHeight / $originalHeight);
|
||||
$newWidth = $originalWidth * $ratio;
|
||||
$newHeight = $originalHeight * $ratio;
|
||||
|
||||
// Centrar imagen
|
||||
$x = (210 - $newWidth) / 2;
|
||||
$y = 25 + (($availableHeight - $newHeight) / 2);
|
||||
|
||||
// Determinar tipo de imagen
|
||||
$imageExtension = '';
|
||||
switch ($imageType) {
|
||||
case IMAGETYPE_JPEG:
|
||||
$imageExtension = 'JPEG';
|
||||
break;
|
||||
case IMAGETYPE_JPEG:
|
||||
$imageExtension = 'JPG';
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
$imageExtension = 'PNG';
|
||||
break;
|
||||
default:
|
||||
// Si no es un formato soportado, continuar
|
||||
unlink($tempPath);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Insertar imagen
|
||||
$pdf->Image($tempPath, $x, $y, $newWidth, $newHeight, $imageExtension);
|
||||
}
|
||||
|
||||
// Limpiar archivo temporal
|
||||
unlink($tempPath);
|
||||
}
|
||||
|
||||
// Verificar que se agregaron páginas
|
||||
if ($pdf->PageNo() == 0) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se pudieron procesar las imágenes del expediente.',
|
||||
'record_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Generar PDF
|
||||
$pdfContent = $pdf->Output('S');
|
||||
|
||||
return response($pdfContent, 200)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-Disposition', 'inline; filename="expediente-imagenes-' . $record->folio . '.pdf"');
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al generar el PDF de imágenes',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function generatePdfForm($id)
|
||||
{
|
||||
try {
|
||||
$record = Record::with([
|
||||
'vehicle',
|
||||
'vehicle.owner',
|
||||
'vehicle.tag',
|
||||
])->findOrFail($id);
|
||||
|
||||
if (!$record->vehicle) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El registro no tiene un vehículo asociado.',
|
||||
'record_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
$vehicle = $record->vehicle;
|
||||
$owner = $vehicle->owner;
|
||||
$tag = $vehicle->tag;
|
||||
|
||||
$now = Carbon::now()->locale('es_MX');
|
||||
|
||||
$data = [
|
||||
// Datos del vehículo
|
||||
'marca' => strtoupper($vehicle->marca ?? ''),
|
||||
'linea' => strtoupper($vehicle->linea ?? ''),
|
||||
'modelo' => $vehicle->modelo ?? '',
|
||||
'niv' => strtoupper($vehicle->niv ?? ''),
|
||||
'numero_motor' => strtoupper($vehicle->numero_motor ?? ''),
|
||||
'placa' => strtoupper($vehicle->placa ?? ''),
|
||||
'folio' => $tag?->folio ?? $record->folio ?? '',
|
||||
|
||||
// Datos del propietario
|
||||
'telefono' => $owner?->telefono ?? '',
|
||||
|
||||
// Fecha actual
|
||||
'fecha' => $now->format('d'),
|
||||
'mes' => ucfirst($now->translatedFormat('F')),
|
||||
'anio' => $now->format('Y'),
|
||||
|
||||
'record_id' => $record->id,
|
||||
'owner_name' => $owner?->full_name ?? '',
|
||||
];
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.form', $data)
|
||||
->setPaper('a4', 'portrait')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('solicitud-sustitucion-' . time() . '.pdf');
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontró el registro del expediente proporcionado.',
|
||||
'record_id' => $id,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al generar el PDF del formulario',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function pdfCancelledTag(Tag $tag)
|
||||
{
|
||||
try {
|
||||
$tag->load('status');
|
||||
|
||||
if(!$tag->status){
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El tag no tiene un estado asociado.',
|
||||
'tag_id' => $tag->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar que el tag esté cancelado
|
||||
if (!$tag->isCancelled()) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Solo se puede generar PDF para tags cancelados.',
|
||||
'current_status' => $tag->status->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener datos de cancelación
|
||||
$cancellationData = $this->cancellationData($tag);
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.tag', [
|
||||
'cancellation' => $cancellationData,
|
||||
])
|
||||
->setPaper('a4', 'portrait')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('constancia_cancelada_' . $tag->tag_number . '.pdf');
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al generar el PDF.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function pdfSubstitutedTag($recordId)
|
||||
{
|
||||
try {
|
||||
// Validar que el tag tenga una sustitución registrada
|
||||
$record = Record::with([
|
||||
'vehicle.vehicleTagLogs' => function ($query){
|
||||
$query->where('action_type', 'sustitucion')
|
||||
->whereNotNull('cancellation_at')
|
||||
->latest();
|
||||
}
|
||||
])->findOrFail($recordId);
|
||||
|
||||
$oldTagLog = $record->vehicle->vehicleTagLogs->first();
|
||||
|
||||
if (!$oldTagLog) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se encontró una sustitución registrada para este expediente.',
|
||||
'record' => $recordId,
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener datos de sustitución
|
||||
$oldTag = Tag::with([
|
||||
'vehicleTagLogs' => function($query){
|
||||
$query->where('action_type', 'sustitucion')
|
||||
->whereNotNull('cancellation_at')
|
||||
->with(['cancellationReason', 'cancelledBy', 'vehicle'])
|
||||
->latest();
|
||||
}
|
||||
])->findOrFail($oldTagLog->tag_id);
|
||||
|
||||
$hasSubstitution = $oldTag->vehicleTagLogs()
|
||||
->where('action_type', 'sustitucion')
|
||||
->whereNotNull('cancellation_at')
|
||||
->exists();
|
||||
|
||||
if(!$hasSubstitution){
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag no tiene sustitución registrada.',
|
||||
'tag' => $oldTag->folio,
|
||||
]);
|
||||
}
|
||||
|
||||
$substitutionData = $this->substitutionData($oldTag);
|
||||
|
||||
$pdf = Pdf::loadView('pdfs.tag_sustitution', [
|
||||
'substitution' => $substitutionData,
|
||||
])
|
||||
->setPaper('a4', 'portrait')
|
||||
->setOptions([
|
||||
'defaultFont' => 'sans-serif',
|
||||
'isHtml5ParserEnabled' => true,
|
||||
'isRemoteEnabled' => true,
|
||||
]);
|
||||
|
||||
return $pdf->stream('constancia_sustituida_' . $oldTag->folio . '.pdf');
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al generar el PDF del tag sustituido.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function cancellationData(Tag $tag)
|
||||
{
|
||||
$data = [
|
||||
'fecha' => now()->format('d/m/Y'),
|
||||
'folio' => $tag->folio ?? '',
|
||||
'id_chip' => '',
|
||||
'placa' => '',
|
||||
'niv' => '',
|
||||
'motivo' => 'N/A',
|
||||
'operador' => 'N/A',
|
||||
'modulo' => '',
|
||||
'ubicacion' => '',
|
||||
];
|
||||
|
||||
// Intentar obtener datos del vehículo si existe
|
||||
if ($tag->vehicle_id && $tag->vehicle) {
|
||||
$data['id_chip'] = $tag->vehicle->id_chip ?? '';
|
||||
$data['placa'] = $tag->vehicle->placa ?? '';
|
||||
$data['niv'] = $tag->vehicle->niv ?? '';
|
||||
}
|
||||
|
||||
// Buscar log de cancelación directa
|
||||
$tagCancellationLog = $tag->cancellationLogs()
|
||||
->with(['cancellationReason', 'cancelledBy'])
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($tagCancellationLog) {
|
||||
$data['fecha'] = $tagCancellationLog->cancellation_at->format('d/m/Y');
|
||||
$data['motivo'] = $tagCancellationLog->cancellationReason->name ?? 'No especificado';
|
||||
$data['operador'] = $tagCancellationLog->cancelledBy->name ?? 'Sistema';
|
||||
|
||||
// Extraer datos adicionales de las observaciones
|
||||
$this->extractAdditionalDataFromObservations($tagCancellationLog->cancellation_observations, $data);
|
||||
|
||||
// Cargar módulo del tag si existe, sino cargar módulo del usuario
|
||||
if ($tag->module_id && $tag->module) {
|
||||
$data['modulo'] = $tag->module->name;
|
||||
$data['ubicacion'] = $tag->module->address;
|
||||
} elseif ($tagCancellationLog->cancelledBy) {
|
||||
$user = $tagCancellationLog->cancelledBy;
|
||||
$this->loadUserModule($user, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Buscar log de vehículo (tag asignado y luego cancelado)
|
||||
$vehicleTagLog = $tag->vehicleTagLogs()
|
||||
->where('action_type', 'cancelacion')
|
||||
->with(['cancellationReason', 'cancelledBy', 'vehicle'])
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($vehicleTagLog) {
|
||||
$data['motivo'] = $vehicleTagLog->cancellationReason->name ?? 'No especificado';
|
||||
$data['operador'] = $vehicleTagLog->cancelledBy->name ?? 'Sistema';
|
||||
|
||||
// Cargar módulo del cual el usuario es responsable
|
||||
if ($vehicleTagLog->cancelledBy) {
|
||||
$user = $vehicleTagLog->cancelledBy;
|
||||
$this->loadUserModule($user, $data);
|
||||
}
|
||||
|
||||
if ($vehicleTagLog->vehicle) {
|
||||
$data['id_chip'] = $vehicleTagLog->vehicle->id_chip ?? '';
|
||||
$data['placa'] = $vehicleTagLog->vehicle->placa ?? '';
|
||||
$data['niv'] = $vehicleTagLog->vehicle->niv ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extraer datos adicionales de las observaciones de cancelación
|
||||
*/
|
||||
private function extractAdditionalDataFromObservations($observations, &$data)
|
||||
{
|
||||
if (empty($observations)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraer ID CHIP
|
||||
if (preg_match('/ID CHIP:\s*([^|]+)/', $observations, $matches)) {
|
||||
$data['id_chip'] = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Extraer PLACA
|
||||
if (preg_match('/PLACA:\s*([^|]+)/', $observations, $matches)) {
|
||||
$data['placa'] = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Extraer VIN
|
||||
if (preg_match('/VIN:\s*([^|]+)/', $observations, $matches)) {
|
||||
$data['niv'] = trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private function substitutionData(Tag $tag)
|
||||
{
|
||||
$data = [
|
||||
'fecha' => now()->format('d/m/Y'),
|
||||
'folio' => $tag->folio ?? '',
|
||||
'folio_sustituto' => '',
|
||||
'id_chip' => '',
|
||||
'placa' => '',
|
||||
'niv' => '',
|
||||
'motivo' => 'N/A',
|
||||
'operador' => 'N/A',
|
||||
'modulo' => '',
|
||||
'ubicacion' => '',
|
||||
];
|
||||
|
||||
// log de CANCELACIÓN del tag original
|
||||
$oldTagLog = $tag->vehicleTagLogs()
|
||||
->where('action_type', 'sustitucion')
|
||||
->whereNotNull('cancellation_at')
|
||||
->with(['cancellationReason', 'cancelledBy', 'vehicle'])
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (!$oldTagLog) {
|
||||
return $data; // No se encontró sustitución
|
||||
}
|
||||
|
||||
// datos del motivo y operador
|
||||
$data['fecha'] = $oldTagLog->cancellation_at->format('d/m/Y');
|
||||
$data['motivo'] = $oldTagLog->cancellationReason->name ?? 'No especificado';
|
||||
$data['operador'] = $oldTagLog->cancelledBy->name ?? 'Sistema';
|
||||
|
||||
// módulo del usuario
|
||||
if ($oldTagLog->cancelledBy) {
|
||||
$this->loadUserModule($oldTagLog->cancelledBy, $data);
|
||||
}
|
||||
|
||||
// datos del vehículo
|
||||
if ($oldTagLog->vehicle) {
|
||||
$data['id_chip'] = $oldTagLog->vehicle->id_chip ?? '';
|
||||
$data['placa'] = $oldTagLog->vehicle->placa ?? '';
|
||||
$data['niv'] = $oldTagLog->vehicle->niv ?? '';
|
||||
|
||||
// tag NUEVO
|
||||
$newTag = $oldTagLog->vehicle->tag;
|
||||
$data['folio_sustituto'] = $newTag?->folio ?? '';
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargar módulo del usuario
|
||||
*/
|
||||
private function loadUserModule($user, &$data)
|
||||
{
|
||||
// Intentar cargar module
|
||||
$user->load('module');
|
||||
|
||||
// Si no tiene module, usar responsibleModule
|
||||
if (!$user->module) {
|
||||
$user->load('responsibleModule');
|
||||
|
||||
if ($user->responsibleModule) {
|
||||
$data['modulo'] = $user->responsibleModule->name;
|
||||
$data['ubicacion'] = $user->responsibleModule->address;
|
||||
}
|
||||
} else {
|
||||
$data['modulo'] = $user->module->name;
|
||||
$data['ubicacion'] = $user->module->address;
|
||||
}
|
||||
}
|
||||
|
||||
public function errors(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'folio' => 'nullable|string',
|
||||
'placa' => 'nullable|string',
|
||||
'vin' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$records = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user', 'error'])
|
||||
->whereNotNull('api_response')
|
||||
->whereRaw("JSON_EXTRACT(api_response, '$.has_error') = true")
|
||||
->orderBy('id', 'ASC');
|
||||
|
||||
if ($request->filled('folio')) {
|
||||
$records->where('folio', 'LIKE', '%' . $request->input('folio') . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('placa')) {
|
||||
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||
$q->where('placa', 'LIKE', '%' . $request->input('placa') . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('vin')) {
|
||||
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||
$q->where('niv', 'LIKE', '%' . $request->input('vin') . '%');
|
||||
});
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Expedientes con errores encontrados exitosamente',
|
||||
'records' => $records->paginate(config('app.pagination')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,589 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CatalogTagStatus;
|
||||
use App\Models\Module;
|
||||
use App\Models\Package;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Tag;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TagsController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$tags = Tag::with([
|
||||
'vehicle:id,placa,niv',
|
||||
'package:id,lot,box_number',
|
||||
'status:id,code,name',
|
||||
'module:id,name'
|
||||
])->orderBy('id', 'ASC');
|
||||
|
||||
if ($request->has('status')) {
|
||||
$tags->whereHas('status', function ($q) use ($request) {
|
||||
$q->where('name', $request->status);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->has('lot')) {
|
||||
$tags->whereHas('package', function ($q) use ($request) {
|
||||
$q->where('lot', $request->lot);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->has('package_id')) {
|
||||
$tags->where('package_id', $request->package_id);
|
||||
}
|
||||
|
||||
if ($request->has('module_id')) {
|
||||
$tags->where('module_id', $request->module_id);
|
||||
}
|
||||
|
||||
$paginatedTags = $tags->paginate(config('app.pagination'));
|
||||
|
||||
// Validación si no hay resultados
|
||||
if ($paginatedTags->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron tags con los criterios de búsqueda proporcionados.',
|
||||
'filters_applied' => array_filter($request->only(['status', 'lot', 'package_id', 'module_id']))
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'tag' => $paginatedTags,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al obtener la lista de tags.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'folio' => 'required|string|max:8',
|
||||
'package_id' => 'required|integer|exists:packages,id',
|
||||
'tag_number' => 'nullable|string|min:32|max:32',
|
||||
'module_id' => 'nullable|integer|exists:modules,id',
|
||||
]);
|
||||
|
||||
// Verificar si ya existe un tag con el mismo folio
|
||||
$existingTagByFolio = Tag::where('folio', $validated['folio'])->first();
|
||||
if ($existingTagByFolio) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: El folio ya existe en el sistema.',
|
||||
'error' => 'folio duplicado',
|
||||
'folio' => $validated['folio'],
|
||||
'existing_tag' => [
|
||||
'id' => $existingTagByFolio->id,
|
||||
'folio' => $existingTagByFolio->folio,
|
||||
'tag_number' => $existingTagByFolio->tag_number,
|
||||
'status' => $existingTagByFolio->status->name ?? null,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// Verificar si ya existe un tag con el mismo tag_number
|
||||
if (isset($validated['tag_number']) && $validated['tag_number'] !== null) {
|
||||
$existingTagByNumber = Tag::where('tag_number', $validated['tag_number'])->first();
|
||||
if ($existingTagByNumber) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: El tag_number ya existe en el sistema.',
|
||||
'error' => 'duplicate_tag_number',
|
||||
'tag_number' => $validated['tag_number'],
|
||||
'existing_tag_id' => $existingTagByNumber->id,
|
||||
'existing_tag' => [
|
||||
'id' => $existingTagByNumber->id,
|
||||
'folio' => $existingTagByNumber->folio,
|
||||
'tag_number' => $existingTagByNumber->tag_number,
|
||||
'status' => $existingTagByNumber->status->name ?? null,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener el status "disponible" por defecto
|
||||
$statusAvailable = CatalogTagStatus::where('code', Tag::STATUS_AVAILABLE)->first();
|
||||
if (!$statusAvailable) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'No se pudo crear el tag: El status "disponible" no existe en el catálogo de estados.',
|
||||
'error' => 'missing_default_status',
|
||||
]);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Obtener el paquete
|
||||
$package = Package::findOrFail($validated['package_id']);
|
||||
$folioNumerico = (int) $validated['folio'];
|
||||
|
||||
// Verificar si el folio está fuera del rango actual del paquete
|
||||
$packageUpdated = false;
|
||||
$rangeChanges = [];
|
||||
$missingTags = [];
|
||||
|
||||
// Caso 1: El folio es MENOR que el starting_page (crear tags intermedios)
|
||||
if ($folioNumerico < $package->starting_page) {
|
||||
$rangeChanges['starting_page'] = [
|
||||
'old' => $package->starting_page,
|
||||
'new' => $folioNumerico,
|
||||
];
|
||||
|
||||
// Crear tags intermedios (desde el nuevo folio hasta el starting_page - 1)
|
||||
for ($i = $folioNumerico + 1; $i < $package->starting_page; $i++) {
|
||||
// Verificar que el tag no exista
|
||||
$existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first();
|
||||
if (!$existingTag) {
|
||||
Tag::create([
|
||||
'folio' => $i,
|
||||
'tag_number' => null,
|
||||
'package_id' => $package->id,
|
||||
'module_id' => null,
|
||||
'status_id' => $statusAvailable->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
$missingTags[] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$package->starting_page = $folioNumerico;
|
||||
$packageUpdated = true;
|
||||
}
|
||||
|
||||
// Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios)
|
||||
if ($folioNumerico > $package->ending_page) {
|
||||
$rangeChanges['ending_page'] = [
|
||||
'old' => $package->ending_page,
|
||||
'new' => $folioNumerico,
|
||||
];
|
||||
|
||||
// Crear tags intermedios (desde ending_page + 1 hasta el nuevo folio - 1)
|
||||
for ($i = $package->ending_page + 1; $i < $folioNumerico; $i++) {
|
||||
// Verificar que el tag no exista
|
||||
$existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first();
|
||||
if (!$existingTag) {
|
||||
Tag::create([
|
||||
'folio' => $i,
|
||||
'tag_number' => null,
|
||||
'package_id' => $package->id,
|
||||
'module_id' => null,
|
||||
'status_id' => $statusAvailable->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
$missingTags[] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$package->ending_page = $folioNumerico;
|
||||
$packageUpdated = true;
|
||||
}
|
||||
|
||||
// Guardar cambios en el paquete si es necesario
|
||||
if ($packageUpdated) {
|
||||
$package->save();
|
||||
}
|
||||
|
||||
// Crear el tag principal solicitado
|
||||
$tag = Tag::create([
|
||||
'folio' => $validated['folio'],
|
||||
'tag_number' => $validated['tag_number'] ?? null,
|
||||
'package_id' => $validated['package_id'],
|
||||
'module_id' => $validated['module_id'] ?? null,
|
||||
'status_id' => $statusAvailable->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Cargar relaciones
|
||||
$tag->load(['package', 'module', 'status']);
|
||||
|
||||
$response = [
|
||||
'message' => 'Tag creado correctamente.',
|
||||
'tag' => $tag,
|
||||
];
|
||||
|
||||
// Agregar información de actualización del paquete si hubo cambios
|
||||
if ($packageUpdated) {
|
||||
$response['package_updated'] = true;
|
||||
$response['package_range_changes'] = $rangeChanges;
|
||||
$response['package_current_range'] = [
|
||||
'starting_page' => $package->starting_page,
|
||||
'ending_page' => $package->ending_page,
|
||||
];
|
||||
}
|
||||
|
||||
// Agregar información de tags intermedios creados
|
||||
if (!empty($missingTags)) {
|
||||
$response['missing_tags_created'] = $missingTags;
|
||||
$response['missing_tags_count'] = count($missingTags);
|
||||
}
|
||||
|
||||
return ApiResponse::CREATED->response($response);
|
||||
} catch (ValidationException $e) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: Datos de validación incorrectos.',
|
||||
'error' => 'validation_error',
|
||||
'errors' => $e->errors(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
// Capturar errores específicos de base de datos
|
||||
$errorMessage = $e->getMessage();
|
||||
|
||||
if (str_contains($errorMessage, 'Duplicate entry') || str_contains($errorMessage, '1062')) {
|
||||
// Intentar identificar qué campo está duplicado
|
||||
if (str_contains($errorMessage, 'folio')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: El folio ya existe en el sistema.',
|
||||
'error' => 'duplicate_folio',
|
||||
'details' => $errorMessage,
|
||||
]);
|
||||
} elseif (str_contains($errorMessage, 'tag_number')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: El tag_number ya existe en el sistema.',
|
||||
'error' => 'duplicate_tag_number',
|
||||
'details' => $errorMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: Ya existe un registro duplicado en el sistema.',
|
||||
'error' => 'duplicate_entry',
|
||||
'details' => $errorMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
if (str_contains($errorMessage, 'Foreign key constraint')) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se pudo crear el tag: Referencia a un registro que no existe (package_id o module_id inválido).',
|
||||
'error' => 'foreign_key_constraint',
|
||||
'details' => $errorMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'No se pudo crear el tag: Error interno del servidor.',
|
||||
'error' => 'internal_error',
|
||||
'details' => $errorMessage,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(Tag $tag)
|
||||
{
|
||||
$tag->load(['package', 'module', 'vehicle', 'status']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'tag' => $tag,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Tag $tag)
|
||||
{
|
||||
try {
|
||||
// Validar que el tag solo pueda actualizarse si está disponible o cancelado
|
||||
if (!in_array($tag->status->code, [Tag::STATUS_AVAILABLE, Tag::STATUS_CANCELLED])) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Solo se pueden actualizar tags con status "disponible" o "cancelado".',
|
||||
'current_status' => $tag->status->name,
|
||||
'allowed_statuses' => ['Disponible', 'Cancelado'],
|
||||
]);
|
||||
}
|
||||
|
||||
// Validar los campos de entrada
|
||||
$validated = $request->validate([
|
||||
'folio' => 'sometimes|string|max:8',
|
||||
'tag_number' => 'nullable|string|min:32|max:32',
|
||||
'package_id' => 'sometimes|integer|exists:packages,id',
|
||||
'module_id' => 'nullable|integer|exists:modules,id',
|
||||
'status_id' => 'sometimes|integer|exists:catalog_tag_status,id',
|
||||
]);
|
||||
|
||||
// Si se va a cambiar el status, validar que solo sea a disponible o cancelado
|
||||
if (isset($validated['status_id'])) {
|
||||
$newStatus = CatalogTagStatus::find($validated['status_id']);
|
||||
if (!in_array($newStatus->code, [Tag::STATUS_AVAILABLE, Tag::STATUS_CANCELLED])) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Solo se puede cambiar el status a disponible o cancelado.',
|
||||
'estatus' => $newStatus->name,
|
||||
'estatus_permitido' => ['Disponible', 'Cancelado'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar unicidad del folio si se está actualizando
|
||||
if (isset($validated['folio'])) {
|
||||
$existingTag = Tag::where('folio', $validated['folio'])
|
||||
->where('id', '!=', $tag->id)
|
||||
->first();
|
||||
|
||||
if ($existingTag) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El folio ya está asignado a otro tag.',
|
||||
'folio' => $validated['folio'],
|
||||
'existing_tag_id' => $existingTag->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar unicidad del tag_number si se está actualizando
|
||||
if (isset($validated['tag_number']) && $validated['tag_number'] !== null) {
|
||||
$existingTag = Tag::where('tag_number', $validated['tag_number'])
|
||||
->where('id', '!=', $tag->id)
|
||||
->first();
|
||||
|
||||
if ($existingTag) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'El tag_number ya está asignado a otro tag.',
|
||||
'tag_number' => $validated['tag_number'],
|
||||
'existing_tag_id' => $existingTag->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Actualizar el tag
|
||||
$tag->update($validated);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Cargar relaciones actualizadas
|
||||
$tag->load(['package', 'module', 'vehicle', 'status']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Tag actualizado correctamente.',
|
||||
'tag' => $tag,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al actualizar el tag.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(Tag $tag)
|
||||
{
|
||||
try {
|
||||
$tag->delete();
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Tag eliminado correctamente.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar el tag.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
public function tagStore(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'package_id' => 'required|integer|exists:packages,id',
|
||||
'tags' => 'required|array',
|
||||
'tags.*.folio' => 'required|string|max:8',
|
||||
'tags.*.tag_number' => 'nullable|string|max:32',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$statusAvailable = CatalogTagStatus::where('code', Tag::STATUS_AVAILABLE)->first();
|
||||
|
||||
if (!$statusAvailable) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'El estado "disponible" no existe en el catálogo de estados.',
|
||||
]);
|
||||
}
|
||||
|
||||
$createdTags = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($request->tags as $index => $tagData) {
|
||||
try {
|
||||
$tag = Tag::create([
|
||||
'folio' => $tagData['folio'],
|
||||
'tag_number' => $tagData['tag_number'] ?? null,
|
||||
'package_id' => $request->package_id,
|
||||
'status_id' => $statusAvailable->id,
|
||||
'vehicle_id' => null,
|
||||
'module_id' => null,
|
||||
]);
|
||||
$createdTags[] = $tag;
|
||||
} catch (Exception $e) {
|
||||
// Detectar error de duplicado
|
||||
$errorMessage = $e->getMessage();
|
||||
if (str_contains($errorMessage, 'Duplicate entry') || str_contains($errorMessage, '1062')) {
|
||||
$errorMessage = 'El tag ya existe en el sistema';
|
||||
}
|
||||
|
||||
$errors[] = [
|
||||
'index' => $index,
|
||||
'folio' => $tagData['folio'],
|
||||
'tag_number' => $tagData['tag_number'],
|
||||
'error' => $errorMessage,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollback();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error al importar tags.',
|
||||
'errors' => $errors,
|
||||
'exitosos' => $createdTags,
|
||||
'fallidos' => count($errors),
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Tags importados correctamente.',
|
||||
'tags' => $createdTags,
|
||||
'total' => count($createdTags),
|
||||
'package' => $request->package_id,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al importar tags.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function assignToModule(Request $request)
|
||||
{
|
||||
try {
|
||||
// Validar parámetros de entrada
|
||||
$request->validate([
|
||||
'module_id' => 'required|integer|exists:modules,id',
|
||||
'package_id' => 'required|integer|exists:packages,id',
|
||||
'cantidad' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
// Buscar el package
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
|
||||
// Obtener el status "disponible"
|
||||
$statusAvailable = CatalogTagStatus::where('code', Tag::STATUS_AVAILABLE)->first();
|
||||
|
||||
if (!$statusAvailable) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'No se encontró el status "disponible" en el catálogo.',
|
||||
'error' => 'missing_status',
|
||||
]);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Buscar tags disponibles en el package específico
|
||||
$tags = Tag::where('package_id', $package->id)
|
||||
->where('status_id', $statusAvailable->id)
|
||||
->whereNull('module_id')
|
||||
->whereNull('vehicle_id')
|
||||
->orderBy('folio', 'ASC')
|
||||
->limit($request->cantidad)
|
||||
->get();
|
||||
|
||||
if ($tags->isEmpty()) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron tags disponibles en el paquete especificado.',
|
||||
'package_id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($tags->count() < $request->cantidad) {
|
||||
DB::rollBack();
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => "Solo hay {$tags->count()} tags disponibles en este paquete, pero solicitaste {$request->cantidad}.",
|
||||
'package_id' => $package->id,
|
||||
'lot' => $package->lot,
|
||||
'box_number' => $package->box_number,
|
||||
'disponibles' => $tags->count(),
|
||||
'solicitados' => $request->cantidad,
|
||||
]);
|
||||
}
|
||||
|
||||
// Asignar módulo a los tags seleccionados
|
||||
$tagIds = $tags->pluck('id')->toArray();
|
||||
Tag::whereIn('id', $tagIds)->update(['module_id' => $request->module_id]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Generar PDF de Vale de Entrega
|
||||
$module = Module::with('users')->findOrFail($request->module_id);
|
||||
$tagsAssigned = Tag::whereIn('id', $tagIds)
|
||||
->with(['package', 'status'])
|
||||
->orderBy('folio', 'ASC')
|
||||
->get();
|
||||
|
||||
$pdf = $this->generateValeEntregaPdf($module, $tagsAssigned);
|
||||
|
||||
return $pdf->download('vale-entrega-modulo-' . $module->id . '-' . date('YmdHis') . '.pdf');
|
||||
} catch (ValidationException $e) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error de validación.',
|
||||
'errors' => $e->errors(),
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontró el paquete especificado.',
|
||||
'package_id' => $request->package_id ?? null,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al asignar tags al módulo.',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar PDF de Vale de Entrega
|
||||
*/
|
||||
private function generateValeEntregaPdf(Module $module, $tags)
|
||||
{
|
||||
// Cargar responsables del módulo con sus roles
|
||||
$responsables = $module->users()->with('roles')->get();
|
||||
|
||||
// Preparar datos para el PDF
|
||||
$data = [
|
||||
'module' => $module,
|
||||
'responsables' => $responsables,
|
||||
'tags' => $tags,
|
||||
'total_tags' => $tags->count(),
|
||||
'fecha' => Carbon::now()->locale('es')->isoFormat('D [de] MMMM [de] YYYY'),
|
||||
];
|
||||
|
||||
//PDF
|
||||
$pdf = Pdf::loadView('pdfs.delivery', $data);
|
||||
$pdf->setPaper('letter', 'portrait');
|
||||
|
||||
return $pdf;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,20 +25,14 @@ class LoginController extends Controller
|
||||
{
|
||||
/**
|
||||
* Iniciar sesión
|
||||
* Permite login con username O email
|
||||
*/
|
||||
public function login(LoginRequest $request)
|
||||
{
|
||||
$credential = $request->get('username');
|
||||
|
||||
// Buscar por username o email
|
||||
$user = User::where('username', $credential)
|
||||
->orWhere('email', $credential)
|
||||
->first();
|
||||
$user = User::where('email', $request->get('email'))->first();
|
||||
|
||||
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
||||
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||
'username' => ['Credenciales inválidas']
|
||||
'email' => ['Usuario no valido']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
<?php namespace App\Http\Controllers\System;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
use App\Enums\SettingTypeEk;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*/
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
|
||||
public function show()
|
||||
{
|
||||
$encryptedCredentials = Setting::value('repuve_federal_credentials');
|
||||
|
||||
if (!$encryptedCredentials) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'username' => '',
|
||||
'password_exists' => false
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$credentials = EncryptionHelper::decryptData($encryptedCredentials);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'username' => $credentials['username'] ?? '',
|
||||
'password_exists' => !empty($credentials['password'])
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'username' => 'required|string|max:255',
|
||||
'password' => 'required|string|min:6|max:255',
|
||||
]);
|
||||
|
||||
// Preparar datos para encriptar
|
||||
$credentials = [
|
||||
'username' => $validated['username'],
|
||||
'password' => $validated['password']
|
||||
];
|
||||
|
||||
// Encriptar las credenciales
|
||||
$encryptedValue = EncryptionHelper::encryptData($credentials);
|
||||
|
||||
// Guardar en BD (crea o actualiza automáticamente)
|
||||
Setting::value(
|
||||
key: 'repuve_federal_credentials',
|
||||
value: $encryptedValue,
|
||||
description: 'Credenciales encriptadas para REPUVE Federal',
|
||||
type_ek: SettingTypeEk::JSON
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Credenciales guardadas correctamente',
|
||||
'data' => [
|
||||
'username' => $credentials['username'],
|
||||
'password_exists' => true
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -30,17 +30,8 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['required', 'string'], // Acepta username o email
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'min:8'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.required' => 'El usuario o email es requerido',
|
||||
'password.required' => 'La contraseña es requerida',
|
||||
'password.min' => 'La contraseña debe tener al menos 8 caracteres',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CancelConstanciaRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'new_folio' => 'required|string',
|
||||
'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id',
|
||||
'cancellation_observations' => 'nullable|string',
|
||||
'new_tag_number' => 'nullable|exists:tags,tag_number',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'record_id.exists' => 'El expediente especificado no existe.',
|
||||
|
||||
'cancellation_reason_id.required' => 'El motivo de cancelación es obligatorio.',
|
||||
'cancellation_reason_id.exists' => 'El motivo de cancelación no es válido.',
|
||||
|
||||
'new_tag_number.exists' => 'El nuevo tag no existe',
|
||||
'folio.required_with' => 'El folio es requerido cuando se proporciona un nuevo tag',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'record_id' => 'id del expediente',
|
||||
'cancellation_reason' => 'motivo de cancelación',
|
||||
'cancellation_observations' => 'observaciones',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CatalogNameImgStoreRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre es requerido',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CatalogNameImgUpdateRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'names' => ['required'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'names.required' => 'El nombre es requerido',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DeviceStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'brand' => ['required', 'string', 'max:255'],
|
||||
'serie' => ['required', 'string', 'unique:devices,serie', 'max:255'],
|
||||
'mac_address' => ['required', 'string', 'unique:devices,mac_address', 'regex:/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/'],
|
||||
'module_id' => ['required', 'exists:modules,id'],
|
||||
'user_id' => ['required', 'array', 'min:1'],
|
||||
'user_id.*' => ['exists:users,id'],
|
||||
'status' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'brand.required' => 'La marca del dispositivo es requerida',
|
||||
'serie.required' => 'El número de serie del dispositivo es requerido',
|
||||
'serie.unique' => 'El número de serie ya está registrado',
|
||||
'mac_address.required' => 'La dirección MAC es requerida',
|
||||
'mac_address.unique' => 'La dirección MAC ya está registrada',
|
||||
'mac_address.regex' => 'La dirección MAC debe tener un formato válido (Ej: 00:1B:44:11:3A:B7)',
|
||||
'module_id.required' => 'El módulo asignado es requerido',
|
||||
'user_id.required' => 'Debe seleccionar al menos un usuario autorizado',
|
||||
'user_id.array' => 'Los usuarios autorizados deben ser un array',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DeviceUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$deviceId = $this->route('device');
|
||||
|
||||
return [
|
||||
'brand' => ['nullable', 'string', 'max:255'],
|
||||
'serie' => ['nullable', 'string', 'unique:devices,serie,' . $deviceId, 'max:255'],
|
||||
'mac_address' => ['nullable', 'string', 'unique:devices,mac_address,' . $deviceId, 'regex:/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/'],
|
||||
'module_id' => ['nullable', 'exists:modules,id'],
|
||||
'user_id' => ['nullable', 'array'],
|
||||
'user_id.*' => ['exists:users,id'],
|
||||
'status' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'brand.required' => 'La marca del dispositivo es requerida',
|
||||
'serie.required' => 'El número de serie del dispositivo es requerido',
|
||||
'module_id.required' => 'El módulo asignado es requerido',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FileStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre es obligatorio',
|
||||
'file.required' => 'El archivo es obligatorio',
|
||||
'file.mimes' => 'El archivo debe ser de tipo: jpeg, png, jpg',
|
||||
'file.max' => 'El archivo no debe superar los 10MB',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ModuleStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'responsible_id' => ['required', 'exists:users,id'],
|
||||
'municipality_id' => 'required|exists:municipalities,id',
|
||||
'address' => ['required', 'string', 'max:255'],
|
||||
'colony' => ['required', 'string', 'max:100'],
|
||||
'cp' => ['nullable', 'string', 'max:10'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'status' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre del módulo es requerido',
|
||||
'name.string' => 'El nombre debe ser una cadena de texto',
|
||||
'name.max' => 'El nombre no debe superar los 255 caracteres',
|
||||
'municipality_id.required' => 'El municipio es requerido',
|
||||
'address.required' => 'La dirección es requerida',
|
||||
'address.string' => 'La dirección debe ser una cadena de texto',
|
||||
'address.max' => 'La dirección no debe superar los 255 caracteres',
|
||||
'colony.required' => 'La colonia es requerida',
|
||||
'colony.string' => 'La colonia debe ser una cadena de texto',
|
||||
'colony.max' => 'La colonia no debe superar los 100 caracteres',
|
||||
'cp.string' => 'El código postal debe ser una cadena de texto',
|
||||
'cp.max' => 'El código postal no debe superar los 10 caracteres',
|
||||
'longitude.numeric' => 'La longitud debe ser un número',
|
||||
'longitude.between' => 'La longitud debe estar entre -180 y 180',
|
||||
'latitude.numeric' => 'La latitud debe ser un número',
|
||||
'latitude.between' => 'La latitud debe estar entre -90 y 90',
|
||||
'status.boolean' => 'El status debe ser un valor booleano',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ModuleUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['nullable', 'string', 'max:50'],
|
||||
'municipality_id' => ['nullable', 'integer', 'exists:municipalities,id'],
|
||||
'responsible_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'address' => ['nullable', 'string', 'max:50'],
|
||||
'colony' => ['nullable', 'string', 'max:100'],
|
||||
'cp' => ['nullable', 'string', 'max:10'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'status' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.string' => 'El nombre debe ser texto',
|
||||
'name.max' => 'El nombre no debe superar los 50 caracteres',
|
||||
'municipality_id.integer' => 'El municipio debe ser un número entero',
|
||||
'municipality_id.exists' => 'El municipio seleccionado no existe',
|
||||
'responsible_id.integer' => 'El responsable debe ser un número entero',
|
||||
'responsible_id.exists' => 'El responsable seleccionado no existe',
|
||||
'address.string' => 'La dirección debe ser texto',
|
||||
'address.max' => 'La dirección no debe superar los 50 caracteres',
|
||||
'colony.string' => 'La colonia debe ser texto',
|
||||
'colony.max' => 'La colonia no debe superar los 100 caracteres',
|
||||
'cp.string' => 'El código postal debe ser texto',
|
||||
'cp.max' => 'El código postal no debe superar los 10 caracteres',
|
||||
'longitude.numeric' => 'La longitud debe ser un número',
|
||||
'longitude.between' => 'La longitud debe estar entre -180 y 180',
|
||||
'latitude.numeric' => 'La latitud debe ser un número',
|
||||
'latitude.between' => 'La latitud debe estar entre -90 y 90',
|
||||
'status.boolean' => 'El status debe ser un valor booleano',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PackageStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'lot' => ['required', 'string'],
|
||||
'box_number' => ['required', 'integer'],
|
||||
'starting_page' => ['required', 'integer', 'min:1'],
|
||||
'ending_page' => ['required', 'integer', 'min:1', 'gte:starting_page'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'lot.required' => 'El lote es requerido',
|
||||
|
||||
'box_number.required' => 'El número de caja es requerido',
|
||||
|
||||
'starting_page.required' => 'La página inicial es requerida',
|
||||
'starting_page.integer' => 'La página inicial debe ser un número',
|
||||
'starting_page.min' => 'La página inicial debe ser al menos 1',
|
||||
|
||||
'ending_page.required' => 'La página final es requerida',
|
||||
'ending_page.integer' => 'La página final debe ser un número',
|
||||
'ending_page.min' => 'La página final debe ser al menos 1',
|
||||
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PackageUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'lot' => ['sometimes', 'string'],
|
||||
'box_number' => ['sometimes', 'integer'],
|
||||
'starting_page' => ['sometimes', 'integer', 'min:1'],
|
||||
'ending_page' => ['sometimes', 'integer', 'min:1', 'gte:starting_page'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'lot.required' => 'El lote es requerido',
|
||||
|
||||
'box_number.required' => 'El número de caja es requerido',
|
||||
|
||||
'starting_page.required' => 'La página inicial es requerida',
|
||||
'starting_page.integer' => 'La página inicial debe ser un número',
|
||||
'starting_page.min' => 'La página inicial debe ser al menos 1',
|
||||
|
||||
'ending_page.required' => 'La página final es requerida',
|
||||
'ending_page.integer' => 'La página final debe ser un número',
|
||||
'ending_page.min' => 'La página final debe ser al menos 1',
|
||||
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RecordSearchRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'folio' => ['nullable', 'string', 'max:50'],
|
||||
'niv' => ['nullable', 'string', 'max:50'],
|
||||
'numero_serie' => ['nullable', 'string', 'max:50'],
|
||||
'fecha_desde' => ['nullable', 'date', 'date_format:Y-m-d'],
|
||||
'fecha_hasta' => ['nullable', 'date', 'date_format:Y-m-d', 'after_or_equal:fecha_desde'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'folio.string' => 'El folio debe ser una cadena de texto',
|
||||
'niv.string' => 'El NIV debe ser una cadena de texto',
|
||||
'numero_serie.string' => 'El número de serie debe ser una cadena de texto',
|
||||
'fecha_desde.date' => 'La fecha desde debe ser una fecha válida',
|
||||
'fecha_desde.date_format' => 'La fecha desde debe tener el formato Y-m-d',
|
||||
'fecha_hasta.date' => 'La fecha hasta debe ser una fecha válida',
|
||||
'fecha_hasta.after_or_equal' => 'La fecha hasta debe ser posterior o igual a la fecha desde',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
if (!$this->filled('folio') &&
|
||||
!$this->filled('niv') &&
|
||||
!$this->filled('numero_serie') &&
|
||||
!$this->filled('fecha_desde')) {
|
||||
$validator->errors()->add(
|
||||
'search',
|
||||
'Debe proporcionar al menos un criterio de búsqueda (folio, niv o fecha_desde)'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VehicleStoreRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'folio' => ['required', 'string', 'max:50'],
|
||||
'tag_number' => ['required', 'string'],
|
||||
'placa' => ['required', 'string', 'max:30'],
|
||||
'telefono' => ['required', 'string', 'max:11'],
|
||||
'files' => ['nullable', 'array', 'min:1'],
|
||||
'files.*' => ['file', 'mimes:jpeg,png,jpg', 'max:10240'],
|
||||
'name_id' => ['nullable', 'array', 'min:1'],
|
||||
'name_id.*' => ['nullable', 'integer', 'exists:catalog_name_img,id']
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'folio.required' => 'El folio es requerido',
|
||||
'folio.string' => 'El folio debe ser una cadena de texto',
|
||||
'tag_number.required' => 'El tag_number es requerido',
|
||||
'placa.required' => 'La placa es requerida',
|
||||
'placa.string' => 'La placa debe ser una cadena de texto',
|
||||
'telefono.required' => 'El teléfono es requerido',
|
||||
'telefono.max' => 'El teléfono no debe superar los 10 caracteres',
|
||||
'files.array' => 'Los archivos deben ser un array',
|
||||
'files.*.file' => 'Cada elemento debe ser un archivo válido',
|
||||
'files.*.mimes' => 'Los archivos deben ser de tipo: jpeg, png, jpg, pdf',
|
||||
'files.*.max' => 'Cada archivo no debe superar los 10MB',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VehicleUpdateRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'folio' => 'nullable|string|max:50|unique:records,folio,' . $this->route('id'),
|
||||
// --- DATOS DEL VEHÍCULO ---
|
||||
'vehicle.placa' => 'nullable|string|max:20',
|
||||
'vehicle.marca' => 'nullable|string|max:100',
|
||||
'vehicle.linea' => 'nullable|string|max:100',
|
||||
'vehicle.sublinea' => 'nullable|string|max:100',
|
||||
'vehicle.modelo' => 'nullable|string',
|
||||
'vehicle.color' => 'nullable|string|max:50',
|
||||
'vehicle.numero_motor' => 'nullable|string|max:50',
|
||||
'vehicle.clase_veh' => 'nullable|string|max:50',
|
||||
'vehicle.tipo_servicio' => 'nullable|string|max:50',
|
||||
'vehicle.rfv' => 'nullable|string|max:50',
|
||||
'vehicle.rfc' => 'nullable|string|max:13',
|
||||
'vehicle.ofcexpedicion' => 'nullable|string|max:100',
|
||||
'vehicle.fechaexpedicion' => 'nullable|date',
|
||||
'vehicle.tipo_veh' => 'nullable|string|max:50',
|
||||
'vehicle.numptas' => 'nullable|string',
|
||||
'vehicle.observac' => 'nullable|string|max:500',
|
||||
'vehicle.cve_vehi' => 'nullable|string|max:50',
|
||||
'vehicle.nrpv' => 'nullable|string|max:50',
|
||||
'vehicle.tipo_mov' => 'nullable|string|max:50',
|
||||
|
||||
// --- DATOS DEL TAG ---
|
||||
'tag.tag_number' => 'nullable|string|max:32',
|
||||
|
||||
// --- DATOS DEL PROPIETARIO ---
|
||||
'owner.name' => 'nullable|string|max:100',
|
||||
'owner.paternal' => 'nullable|string|max:100',
|
||||
'owner.maternal' => 'nullable|string|max:100',
|
||||
'owner.rfc' => 'nullable|string|max:13',
|
||||
'owner.curp' => 'nullable|string|max:18',
|
||||
'owner.address' => 'nullable|string|max:255',
|
||||
'owner.tipopers' => 'nullable|boolean',
|
||||
'owner.pasaporte' => 'nullable|string|max:20',
|
||||
'owner.licencia' => 'nullable|string|max:20',
|
||||
'owner.ent_fed' => 'nullable|string|max:50',
|
||||
'owner.munic' => 'nullable|string|max:100',
|
||||
'owner.callep' => 'nullable|string|max:100',
|
||||
'owner.num_ext' => 'nullable|string|max:10',
|
||||
'owner.num_int' => 'nullable|string|max:10',
|
||||
'owner.colonia' => 'nullable|string|max:100',
|
||||
'owner.cp' => 'nullable|string|max:5',
|
||||
'owner.telefono' => 'nullable|string|max:15',
|
||||
|
||||
// --- ARCHIVOS ---
|
||||
'files' => 'nullable|array|min:1',
|
||||
'files.*' => 'file|mimes:jpeg,png,jpg|max:2048',
|
||||
'name_id' => 'nullable|array',
|
||||
'name_id.*' => 'integer|exists:catalog_name_img,id',
|
||||
'observations' => 'nullable|array',
|
||||
'observations.*' => 'nullable|string|max:500',
|
||||
'delete_files' => 'nullable|array',
|
||||
'delete_files.*' => 'integer|exists:files,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'vehicle.modelo.string' => 'El modelo debe ser texto',
|
||||
'vehicle.numptas.string' => 'El número de puertas debe ser texto',
|
||||
'owner.tipopers.boolean' => 'El tipo de persona debe ser física o Moral',
|
||||
'owner.cp.max' => 'El código postal debe tener máximo 5 caracteres',
|
||||
'files.*.mimes' => 'Solo se permiten archivos JPG, PNG o JPEG',
|
||||
'files.*.max' => 'El archivo no debe superar 2MB',
|
||||
'observations.*.max' => 'La observación no debe superar 120 caracteres',
|
||||
'delete_files.*.exists' => 'El archivo a eliminar no existe',
|
||||
'folio.unique' => 'El folio ya existe en el sistema',
|
||||
'folio.max' => 'El folio no puede exceder 50 caracteres',
|
||||
'tag.tag_number.max' => 'El tag_number no puede exceder 32 caracteres',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved
|
||||
*/
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
@ -40,21 +39,4 @@ public function rules(): array
|
||||
'roles' => ['nullable', 'array']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparar datos antes de la validación
|
||||
* Genera el username automáticamente
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('name') && $this->has('paternal')) {
|
||||
$this->merge([
|
||||
'username' => User::generateUsername(
|
||||
$this->input('name'),
|
||||
$this->input('paternal'),
|
||||
$this->input('maternal')
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
<?php namespace App\Jobs;
|
||||
|
||||
use App\Models\Error;
|
||||
use App\Models\Record;
|
||||
use App\Services\RepuveService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
class ProcessRepuveResponse implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 2;
|
||||
public int $timeout = 250;
|
||||
public array $backoff = [30];
|
||||
|
||||
/**
|
||||
* Crear instancia del trabajo
|
||||
*/
|
||||
public function __construct(
|
||||
public int $recordId,
|
||||
public array $responseData
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecutar el trabajo
|
||||
*/
|
||||
public function handle(RepuveService $repuveService): void
|
||||
{
|
||||
$record = Record::findOrFail($this->recordId);
|
||||
|
||||
Log::info('ProcessRepuveResponse: Enviando inscripción a REPUVE Nacional...', [
|
||||
'niv' => $this->responseData['niv'] ?? 'N/A',
|
||||
'placa' => $this->responseData['placa'] ?? 'N/A',
|
||||
]);
|
||||
|
||||
$apiResponse = $repuveService->inscribirVehiculo($this->responseData);
|
||||
|
||||
Log::info('ProcessRepuveResponse: Respuesta recibida de REPUVE', [
|
||||
'has_error' => $apiResponse['has_error'],
|
||||
'error_code' => $apiResponse['error_code'] ?? null,
|
||||
'timestamp' => $apiResponse['timestamp'] ?? null,
|
||||
]);
|
||||
|
||||
if($apiResponse['has_error']){
|
||||
$error = Error::where('code', $apiResponse['error_code'])->first();
|
||||
|
||||
Log::error('ProcessRepuveResponse: Error en respuesta REPUVE', [
|
||||
'error_code' => $apiResponse['error_code'],
|
||||
'error_message' => $apiResponse['error_message'] ?? 'Sin mensaje',
|
||||
'error_found_in_db' => $error ? 'Sí' : 'No',
|
||||
]);
|
||||
|
||||
$record->update([
|
||||
'error_id' => $error?->id,
|
||||
'api_response' => $apiResponse,
|
||||
'error_occurred_at' => now(),
|
||||
]);
|
||||
|
||||
Log::warning('ProcessRepuveResponse: Record actualizado con error', [
|
||||
'record_id' => $record->id,
|
||||
'error_id' => $error?->id,
|
||||
]);
|
||||
} else {
|
||||
$record->update([
|
||||
'error_id' => null,
|
||||
'api_response' => $apiResponse,
|
||||
'error_occurred_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::critical('ProcessRepuveResponse: Job FALLÓ después de todos los intentos', [
|
||||
'record_id' => $this->recordId,
|
||||
'exception_class' => get_class($exception),
|
||||
'exception_message' => $exception->getMessage(),
|
||||
'exception_file' => $exception->getFile(),
|
||||
'exception_line' => $exception->getLine(),
|
||||
'attempts' => $this->attempts(),
|
||||
]);
|
||||
|
||||
$record = Record::find($this->recordId);
|
||||
if($record){
|
||||
Log::info('ProcessRepuveResponse: Buscando error genérico código -1');
|
||||
|
||||
$error = Error::where('code', '-1')->first();
|
||||
|
||||
if(!$error){
|
||||
Log::warning('ProcessRepuveResponse: Error código -1 NO encontrado en BD');
|
||||
}
|
||||
|
||||
$record->update([
|
||||
'error_id' => $error?->id,
|
||||
'api_response' => [
|
||||
'has_error' => true,
|
||||
'error_message' => $exception->getMessage(),
|
||||
],
|
||||
'error_occurred_at' => now(),
|
||||
]);
|
||||
|
||||
Log::error('ProcessRepuveResponse: Record actualizado con error crítico', [
|
||||
'record_id' => $record->id,
|
||||
'error_id' => $error?->id,
|
||||
]);
|
||||
} else {
|
||||
Log::error('ProcessRepuveResponse: Record NO encontrado', [
|
||||
'record_id' => $this->recordId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CatalogCancellationReason extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'applies_to'
|
||||
];
|
||||
|
||||
/**
|
||||
* Obtener razones para cancelación
|
||||
*/
|
||||
public function scopeForCancellation($query)
|
||||
{
|
||||
return $query->whereIn('applies_to', ['cancelacion', 'ambos']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener razones para sustitución
|
||||
*/
|
||||
public function scopeForSubstitution($query)
|
||||
{
|
||||
return $query->whereIn('applies_to', ['sustitucion', 'ambos']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs que usan esta razón
|
||||
*/
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class, 'cancellation_reason_id');
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CatalogNameImg extends Model
|
||||
{
|
||||
protected $table = 'catalog_name_img';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
];
|
||||
|
||||
public function files()
|
||||
{
|
||||
return $this->hasMany(File::class, 'name_id');
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Catálogo de estatus de tags
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Device extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'brand',
|
||||
'serie',
|
||||
'mac_address',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function modules()
|
||||
{
|
||||
return $this->belongsToMany(Module::class, 'device_module')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function deviceModules()
|
||||
{
|
||||
return $this->hasMany(DeviceModule::class);
|
||||
}
|
||||
|
||||
public function activeModules()
|
||||
{
|
||||
return $this->belongsToMany(Module::class, 'device_module')
|
||||
->wherePivot('status', true)
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DeviceModule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'device_module';
|
||||
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'module_id',
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function device()
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(Module::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Error extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'type'
|
||||
];
|
||||
|
||||
public function records()
|
||||
{
|
||||
return $this->hasMany(Record::class);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name_id',
|
||||
'path',
|
||||
'md5',
|
||||
'observations',
|
||||
'record_id',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'url',
|
||||
];
|
||||
|
||||
|
||||
public function record()
|
||||
{
|
||||
return $this->belongsTo(Record::class);
|
||||
}
|
||||
|
||||
public function catalogName()
|
||||
{
|
||||
return $this->belongsTo(CatalogNameImg::class, 'name_id');
|
||||
}
|
||||
|
||||
public function url(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => Storage::disk('public')->url($this->path),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Module extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'responsible_id',
|
||||
'municipality_id',
|
||||
'address',
|
||||
'colony',
|
||||
'cp',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'longitude' => 'decimal:8',
|
||||
'latitude' => 'decimal:8',
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function municipality()
|
||||
{
|
||||
return $this->belongsTo(Municipality::class);
|
||||
}
|
||||
|
||||
public function tags(){
|
||||
return $this->hasMany(Tag::class, 'module_id');
|
||||
}
|
||||
|
||||
public function devices()
|
||||
{
|
||||
return $this->belongsTo(Device::class, 'device_module')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function deviceModules()
|
||||
{
|
||||
return $this->hasMany(DeviceModule::class);
|
||||
}
|
||||
|
||||
public function activeDevices()
|
||||
{
|
||||
return $this->belongsToMany(Device::class, 'device_module')
|
||||
->wherePivot('status', true)
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function responsible()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'responsible_id');
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class, 'module_id');
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Municipality extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
];
|
||||
|
||||
public function modules()
|
||||
{
|
||||
return $this->hasMany(Module::class);
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Owner extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'rfc',
|
||||
'curp',
|
||||
'address',
|
||||
'tipopers',
|
||||
'pasaporte',
|
||||
'licencia',
|
||||
'ent_fed',
|
||||
'munic',
|
||||
'callep',
|
||||
'num_ext',
|
||||
'num_int',
|
||||
'colonia',
|
||||
'cp',
|
||||
'telefono',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'full_name',
|
||||
];
|
||||
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => trim("{$this->name} {$this->paternal} {$this->maternal}")
|
||||
);
|
||||
}
|
||||
|
||||
public function vehicles()
|
||||
{
|
||||
return $this->hasMany(Vehicle::class);
|
||||
}
|
||||
|
||||
public function municipality()
|
||||
{
|
||||
return $this->belongsTo(Municipality::class, 'munic', 'code');
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Package extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'lot',
|
||||
'box_number',
|
||||
'starting_page',
|
||||
'ending_page',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'starting_page' => 'integer',
|
||||
'ending_page' => 'integer',
|
||||
];
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(Tag::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Record extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'folio',
|
||||
'vehicle_id',
|
||||
'user_id',
|
||||
'module_id',
|
||||
'error_id',
|
||||
'api_response',
|
||||
'error_occurred_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'api_response' => 'array',
|
||||
'error_occurred_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function vehicle()
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function files()
|
||||
{
|
||||
return $this->hasMany(File::class);
|
||||
}
|
||||
|
||||
public function error()
|
||||
{
|
||||
return $this->belongsTo(Error::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(Module::class);
|
||||
}
|
||||
|
||||
public function vehicleTagLog()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
VehicleTagLog::class,
|
||||
Vehicle::class,
|
||||
'id',
|
||||
'vehicle_id',
|
||||
'vehicle_id',
|
||||
'id'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScanHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'scan_history';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'tag_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relación con User
|
||||
* Un escaneo pertenece a un usuario
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con Tag
|
||||
* Un escaneo pertenece a una etiqueta
|
||||
*/
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// Constantes de status
|
||||
const STATUS_AVAILABLE = 'available';
|
||||
const STATUS_ASSIGNED = 'assigned';
|
||||
const STATUS_CANCELLED = 'cancelled';
|
||||
const STATUS_DAMAGED = 'damaged';
|
||||
|
||||
protected $fillable = [
|
||||
'folio',
|
||||
'tag_number',
|
||||
'vehicle_id',
|
||||
'package_id',
|
||||
'module_id',
|
||||
'status_id',
|
||||
];
|
||||
|
||||
public function vehicle()
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function package()
|
||||
{
|
||||
return $this->belongsTo(Package::class);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(CatalogTagStatus::class, 'status_id');
|
||||
}
|
||||
|
||||
public function module(){
|
||||
return $this->belongsTo(Module::class, 'module_id');
|
||||
}
|
||||
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class);
|
||||
}
|
||||
|
||||
public function scanHistories()
|
||||
{
|
||||
return $this->hasMany(ScanHistory::class);
|
||||
}
|
||||
|
||||
public function cancellationLogs()
|
||||
{
|
||||
return $this->hasMany(TagCancellationLog::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar tag como asignado a un vehículo
|
||||
*/
|
||||
public function markAsAssigned(int $vehicleId, string $folio): void
|
||||
{
|
||||
$statusAssigned = CatalogTagStatus::where('code', self::STATUS_ASSIGNED)->first();
|
||||
|
||||
$this->update([
|
||||
'vehicle_id' => $vehicleId,
|
||||
'folio' => $folio,
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar tag como dañado
|
||||
*/
|
||||
public function markAsDamaged(): void
|
||||
{
|
||||
$statusDamaged = CatalogTagStatus::where('code', self::STATUS_DAMAGED)->first();
|
||||
|
||||
$this->update([
|
||||
'status_id' => $statusDamaged->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está disponible
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_AVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está asignado
|
||||
*/
|
||||
public function isAssigned(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_ASSIGNED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está cancelado
|
||||
*/
|
||||
public function isCancelled(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está dañado
|
||||
*/
|
||||
public function isDamaged(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_DAMAGED;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Log de cancelación de tags no asignados
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class TagCancellationLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'tag_cancellation_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'tag_id',
|
||||
'cancellation_reason_id',
|
||||
'cancellation_observations',
|
||||
'cancellation_at',
|
||||
'cancelled_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'cancellation_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
// Relaciones
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
|
||||
public function cancellationReason()
|
||||
{
|
||||
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
|
||||
}
|
||||
|
||||
public function cancelledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
@ -45,12 +42,10 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'username',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'profile_photo_path',
|
||||
'module_id'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -132,103 +127,4 @@ public function resetPasswords()
|
||||
{
|
||||
return $this->hasMany(ResetPassword::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(Module::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Módulo del cual el usuario es responsable
|
||||
*/
|
||||
public function responsibleModule()
|
||||
{
|
||||
return $this->hasOne(Module::class, 'responsible_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar username automático al crear usuario
|
||||
* Formato: inicial nombre + inicial segundo nombre + apellido paterno + inicial materno
|
||||
*/
|
||||
public static function generateUsername(?string $name, ?string $paternal, ?string $maternal = null): string
|
||||
{
|
||||
// Validar que al menos tengamos nombre y apellido paterno
|
||||
if (empty($name) || empty($paternal)) {
|
||||
return self::ensureUniqueUsername('user');
|
||||
}
|
||||
|
||||
$name = self::normalizeString($name);
|
||||
$paternal = self::normalizeString($paternal);
|
||||
$maternal = $maternal ? self::normalizeString($maternal) : '';
|
||||
|
||||
// Separar nombres y obtener iniciales
|
||||
$nameParts = preg_split('/\s+/', trim($name));
|
||||
$firstInitial = !empty($nameParts[0]) ? substr($nameParts[0], 0, 1) : '';
|
||||
$secondInitial = isset($nameParts[1]) && !empty($nameParts[1]) ? substr($nameParts[1], 0, 1) : '';
|
||||
$maternalInitial = !empty($maternal) ? substr($maternal, 0, 1) : '';
|
||||
|
||||
// Construir username
|
||||
$baseUsername = $firstInitial . $secondInitial . $paternal . $maternalInitial;
|
||||
|
||||
// Si el username queda vacío, usar fallback
|
||||
if (empty($baseUsername)) {
|
||||
$baseUsername = 'user';
|
||||
}
|
||||
|
||||
return self::ensureUniqueUsername($baseUsername);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizar string: quitar acentos, convertir a minúsculas, solo letras
|
||||
*/
|
||||
private static function normalizeString(string $string): string
|
||||
{
|
||||
// Convertir a minúsculas
|
||||
$string = mb_strtolower($string);
|
||||
|
||||
// Reemplazar caracteres acentuados
|
||||
$replacements = [
|
||||
'á' => 'a',
|
||||
'é' => 'e',
|
||||
'í' => 'i',
|
||||
'ó' => 'o',
|
||||
'ú' => 'u',
|
||||
'ä' => 'a',
|
||||
'ë' => 'e',
|
||||
'ï' => 'i',
|
||||
'ö' => 'o',
|
||||
'ü' => 'u',
|
||||
'à' => 'a',
|
||||
'è' => 'e',
|
||||
'ì' => 'i',
|
||||
'ò' => 'o',
|
||||
'ù' => 'u',
|
||||
'ñ' => 'n',
|
||||
'ç' => 'c',
|
||||
];
|
||||
|
||||
$string = strtr($string, $replacements);
|
||||
|
||||
// Eliminar cualquier caracter que no sea letra o espacio
|
||||
$string = preg_replace('/[^a-z\s]/', '', $string);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asegurar que el username sea único, agregando número si es necesario
|
||||
*/
|
||||
private static function ensureUniqueUsername(string $baseUsername): string
|
||||
{
|
||||
$username = $baseUsername;
|
||||
$counter = 1;
|
||||
|
||||
while (self::where('username', $username)->exists()) {
|
||||
$counter++;
|
||||
$username = $baseUsername . $counter;
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Vehicle extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'vehicle';
|
||||
|
||||
protected $fillable = [
|
||||
'placa',
|
||||
'niv',
|
||||
'marca',
|
||||
'linea',
|
||||
'sublinea',
|
||||
'modelo',
|
||||
'color',
|
||||
'numero_motor',
|
||||
'clase_veh',
|
||||
'tipo_servicio',
|
||||
'rfv',
|
||||
'ofcexpedicion',
|
||||
'fechaexpedicion',
|
||||
'tipo_veh',
|
||||
'numptas',
|
||||
'observac',
|
||||
'cve_vehi',
|
||||
'nrpv',
|
||||
'tipo_mov',
|
||||
'owner_id',
|
||||
'reporte_robo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fechaexpedicion' => 'date',
|
||||
'reporte_robo' => 'boolean',
|
||||
];
|
||||
|
||||
public function owner()
|
||||
{
|
||||
return $this->belongsTo(Owner::class);
|
||||
}
|
||||
|
||||
public function records()
|
||||
{
|
||||
return $this->hasMany(Record::class);
|
||||
}
|
||||
|
||||
public function tag()
|
||||
{
|
||||
return $this->hasOne(Tag::class);
|
||||
}
|
||||
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class);
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VehicleTagLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'vehicle_tags_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'vehicle_id',
|
||||
'tag_id',
|
||||
'action_type',
|
||||
'cancellation_reason_id',
|
||||
'cancellation_observations',
|
||||
'cancellation_at',
|
||||
'cancelled_by',
|
||||
'performed_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'cancellation_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function vehicle() {
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function tag() {
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
|
||||
public function cancelledBy() {
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
|
||||
public function cancellationReason()
|
||||
{
|
||||
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
|
||||
}
|
||||
|
||||
public function isInscription()
|
||||
{
|
||||
return $this->action_type === 'inscripcion';
|
||||
}
|
||||
|
||||
public function isUpdate()
|
||||
{
|
||||
return $this->action_type === 'actualizacion';
|
||||
}
|
||||
|
||||
public function isSubstitution()
|
||||
{
|
||||
return $this->action_type === 'sustitucion';
|
||||
}
|
||||
|
||||
public function isCancellation()
|
||||
{
|
||||
return $this->action_type === 'cancelacion';
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PadronEstatalService
|
||||
{
|
||||
private string $soapUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->soapUrl = config('services.padron_estatal.url');
|
||||
}
|
||||
|
||||
public function getVehiculoByNiv(string $niv): array
|
||||
{
|
||||
return $this->consultarPadron('niv', $niv);
|
||||
}
|
||||
|
||||
public function getVehiculoByPlaca(string $placa): array
|
||||
{
|
||||
return $this->consultarPadron('placa', $placa);
|
||||
}
|
||||
|
||||
public function getVehiculoByFolio(string $folio): array
|
||||
{
|
||||
return $this->consultarPadron('folio', $folio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta el padrón vehicular estatal
|
||||
*/
|
||||
private function consultarPadron(string $tipo, string $valor): array
|
||||
{
|
||||
// Construir el Data en formato JSON
|
||||
$data = json_encode([
|
||||
'tipo' => $tipo,
|
||||
'valor' => $valor
|
||||
]);
|
||||
|
||||
// Construir el cuerpo SOAP
|
||||
$soapBody = <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://mx/tgc/ConsultaPadronVehicular.wsdl">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<wsdl:getVehiculosRepuve>
|
||||
<Data>{$data}</Data>
|
||||
</wsdl:getVehiculosRepuve>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
|
||||
// Configurar cURL
|
||||
$ch = curl_init($this->soapUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: ""',
|
||||
'Content-Length: ' . strlen($soapBody)
|
||||
]);
|
||||
|
||||
try {
|
||||
// Ejecutar la petición
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("Error en la petición al padrón estatal: {$error}");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Error HTTP {$httpCode} al consultar padrón estatal");
|
||||
}
|
||||
|
||||
// Parsear la respuesta
|
||||
return $this->parsearRespuesta($response);
|
||||
} finally {
|
||||
unset($ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea la respuesta del padrón estatal
|
||||
*/
|
||||
private function parsearRespuesta(string $soapResponse): array
|
||||
{
|
||||
// Extraer el contenido del tag
|
||||
preg_match('/<result>(.*?)<\/result>/s', $soapResponse, $matches);
|
||||
|
||||
if (!isset($matches[1])) {
|
||||
throw new Exception("No se pudo extraer el resultado del padrón estatal");
|
||||
}
|
||||
|
||||
$jsonContent = trim($matches[1]);
|
||||
|
||||
// Decodificar el JSON
|
||||
$result = json_decode($jsonContent, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Error al decodificar JSON del padrón estatal: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
// La respuesta es un array con un objeto que tiene error y datos
|
||||
if (!isset($result[0])) {
|
||||
throw new Exception("Formato de respuesta inesperado del padrón estatal");
|
||||
}
|
||||
|
||||
$data = $result[0];
|
||||
|
||||
// Verificar si hay error
|
||||
if ($data['error'] !== 0) {
|
||||
throw new Exception("Error en consulta al padrón estatal: código {$data['error']}");
|
||||
}
|
||||
|
||||
// Verificar si hay datos
|
||||
if (!isset($data['datos'][0])) {
|
||||
throw new Exception("No se encontraron datos del vehículo en el padrón estatal");
|
||||
}
|
||||
|
||||
return $data['datos'][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae los datos del vehículo del resultado
|
||||
*/
|
||||
public function extraerDatosVehiculo(array $datos): array
|
||||
{
|
||||
// Convertir fecha de DD/MM/YYYY a YYYY-MM-DD
|
||||
$fechaexpedicion = null;
|
||||
if (isset($datos['fechaexp']) && $datos['fechaexp']) {
|
||||
$fechaexpedicion = $this->convertirFecha($datos['fechaexp']);
|
||||
}
|
||||
|
||||
return [
|
||||
'placa' => $datos['placa'] ?? null,
|
||||
'niv' => $datos['niv'] ?? null,
|
||||
'marca' => $datos['marca'] ?? null,
|
||||
'linea' => $datos['submarca'] ?? null,
|
||||
'sublinea' => $datos['version'] ?? null,
|
||||
'modelo' => $datos['modelo'] ?? null,
|
||||
'color' => $datos['color'] ?? null,
|
||||
'numero_motor' => $datos['motor'] ?? null,
|
||||
'clase_veh' => $datos['clase_veh'] ?? null,
|
||||
'tipo_servicio' => $datos['tipo_uso'] ?? null,
|
||||
'rfv' => $datos['rfv'] ?? null,
|
||||
'ofcexpedicion' => $datos['ofcexp'] ?? null,
|
||||
'fechaexpedicion' => $fechaexpedicion,
|
||||
'tipo_veh' => $datos['tipo_veh'] ?? null,
|
||||
'numptas' => $datos['numptas'] ?? null,
|
||||
'observac' => $datos['observac'] ?? null,
|
||||
'cve_vehi' => $datos['cve_vehi'] ?? null,
|
||||
'nrpv' => $datos['nrpv'] ?? null,
|
||||
'tipo_mov' => $datos['tipo_mov'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte fecha de DD/MM/YYYY a YYYY-MM-DD
|
||||
*/
|
||||
private function convertirFecha(?string $fecha): ?string
|
||||
{
|
||||
if (!$fecha) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si ya está en formato YYYY-MM-DD, retornar tal cual
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
|
||||
return $fecha;
|
||||
}
|
||||
|
||||
// Convertir de DD/MM/YYYY a YYYY-MM-DD
|
||||
if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $fecha, $matches)) {
|
||||
return "{$matches[3]}-{$matches[2]}-{$matches[1]}";
|
||||
}
|
||||
|
||||
// Si no coincide con ningún formato esperado, retornar null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae los datos del propietario del resultado
|
||||
*/
|
||||
public function extraerDatosPropietario(array $datos): array
|
||||
{
|
||||
// Construir dirección completa
|
||||
$addressParts = array_filter([
|
||||
$datos['callep'] ?? null,
|
||||
isset($datos['num_ext']) && $datos['num_ext'] ? "Num {$datos['num_ext']}" : null,
|
||||
isset($datos['num_int']) && $datos['num_int'] ? "Int {$datos['num_int']}" : null,
|
||||
$datos['colonia'] ?? null,
|
||||
isset($datos['cp']) && $datos['cp'] ? "CP {$datos['cp']}" : null,
|
||||
isset($datos['munic']) && $datos['munic'] ? "Mun {$datos['munic']}" : null,
|
||||
isset($datos['ent_fed']) && $datos['ent_fed'] ? "Edo {$datos['ent_fed']}" : null,
|
||||
]);
|
||||
|
||||
$address = implode(', ', $addressParts);
|
||||
|
||||
return [
|
||||
'name' => $datos['nombre'] ?? null,
|
||||
'paternal' => $datos['ap_paterno'] ?? null,
|
||||
'maternal' => $datos['ap_materno'] ?? null,
|
||||
'rfc' => $datos['rfc'] ?? null,
|
||||
'curp' => $datos['curp'] ?? null,
|
||||
'address' => $address,
|
||||
'tipopers' => $datos['tipopers'] ?? null,
|
||||
'pasaporte' => $datos['pasaporte'] ?? null,
|
||||
'licencia' => $datos['licencia'] ?? null,
|
||||
'ent_fed' => $datos['ent_fed'] ?? null,
|
||||
'munic' => $datos['munic'] ?? null,
|
||||
'callep' => $datos['callep'] ?? null,
|
||||
'num_ext' => $datos['num_ext'] ?? null,
|
||||
'num_int' => $datos['num_int'] ?? null,
|
||||
'colonia' => $datos['colonia'] ?? null,
|
||||
'cp' => $datos['cp'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,829 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
use App\Models\Error;
|
||||
use App\Models\Setting;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
|
||||
class RepuveService
|
||||
{
|
||||
private string $baseUrl;
|
||||
private string $roboEndpoint;
|
||||
private string $inscripcionEndpoint;
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private bool $credentialsLoaded = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->baseUrl = config('services.repuve_federal.base_url');
|
||||
$this->roboEndpoint = config('services.repuve_federal.robo_endpoint');
|
||||
$this->inscripcionEndpoint = config('services.repuve_federal.inscripcion_endpoint');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asegurar que las credenciales estén cargadas (lazy loading)
|
||||
*/
|
||||
private function asegurarCargaCredenciales(): void
|
||||
{
|
||||
if ($this->credentialsLoaded) {
|
||||
return; // Ya están cargadas
|
||||
}
|
||||
|
||||
$this->loadCredentials();
|
||||
$this->credentialsLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargar credenciales desde BD
|
||||
*/
|
||||
private function loadCredentials(): void
|
||||
{
|
||||
try {
|
||||
// Obtener credenciales encriptadas desde BD
|
||||
$encryptedCredentials = Setting::value('repuve_federal_credentials');
|
||||
|
||||
if (!$encryptedCredentials) {
|
||||
throw new Exception('Credenciales REPUVE no configuradas en el sistema');
|
||||
}
|
||||
|
||||
$credentials = EncryptionHelper::decryptData($encryptedCredentials);
|
||||
|
||||
if (!$credentials || !isset($credentials['username'], $credentials['password'])) {
|
||||
throw new Exception('Error al desencriptar credenciales REPUVE');
|
||||
}
|
||||
|
||||
$this->username = $credentials['username'];
|
||||
$this->password = $credentials['password'];
|
||||
|
||||
logger()->info('RepuveService: Credenciales cargadas correctamente desde BD');
|
||||
} catch (Exception $e) {
|
||||
logger()->error('RepuveService: Error al cargar credenciales', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw new Exception('No se pudieron cargar las credenciales REPUVE: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function consultarPadron(string $niv)
|
||||
{
|
||||
$this->asegurarCargaCredenciales();
|
||||
|
||||
$url = $this->baseUrl . $this->roboEndpoint;
|
||||
$arg2 = $niv . '|||||||';
|
||||
|
||||
$soapBody = <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<wsdl:doConsPadron>
|
||||
<arg0>{$this->username}</arg0>
|
||||
<arg1>{$this->password}</arg1>
|
||||
<arg2>{$arg2}</arg2>
|
||||
</wsdl:doConsPadron>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: "doConsPadron"',
|
||||
'Content-Length: ' . strlen($soapBody),
|
||||
]);
|
||||
|
||||
try {
|
||||
// Ejecutar la solicitud
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("Error en la petición SOAP: {$error}");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Error al consultar REPUVE: Código HTTP {$httpCode}");
|
||||
}
|
||||
|
||||
return $this->parseVehicleResponse($response, $niv);
|
||||
} finally {
|
||||
unset($ch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function parseVehicleResponse(string $soapResponse, string $niv)
|
||||
{
|
||||
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||
|
||||
if (!isset($matches[1])) {
|
||||
$errorFromDb = Error::where('code', '108')->first();
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => '108',
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'niv' => $niv,
|
||||
'repuve_response' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$contenido = trim($matches[1]);
|
||||
|
||||
// Verificar si hay error de REPUVE Nacional (cualquier formato con ERR o ERROR)
|
||||
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||
$errorCode = $errorMatch[2];
|
||||
|
||||
// Buscar el error completo en la base de datos
|
||||
$errorFromDb = Error::where('code', $errorCode)->first();
|
||||
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => $errorCode,
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? "Error código {$errorCode} - no catalogado",
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'niv' => $niv,
|
||||
'repuve_response' => $contenido,
|
||||
];
|
||||
}
|
||||
|
||||
// Si empieza con OK:, parsear los datos
|
||||
if (str_starts_with($contenido, 'OK:')) {
|
||||
$datos = str_replace('OK:', '', $contenido);
|
||||
$valores = explode('|', $datos);
|
||||
|
||||
$campos = [
|
||||
'marca',
|
||||
'submarca',
|
||||
'tipo_vehiculo',
|
||||
'fecha_expedicion',
|
||||
'oficina',
|
||||
'niv',
|
||||
'placa',
|
||||
'motor',
|
||||
'modelo',
|
||||
'color',
|
||||
'version',
|
||||
'entidad',
|
||||
'marca_padron',
|
||||
'submarca_padron',
|
||||
'tipo_uso_padron',
|
||||
'tipo_vehiculo_padron',
|
||||
'estatus_registro',
|
||||
'aduana',
|
||||
'nombre_aduana',
|
||||
'patente',
|
||||
'pedimento',
|
||||
'fecha_pedimento',
|
||||
'clave_importador',
|
||||
'observaciones'
|
||||
];
|
||||
|
||||
$jsonResponse = [];
|
||||
foreach ($campos as $i => $campo) {
|
||||
$jsonResponse[$campo] = $valores[$i] ?? null;
|
||||
}
|
||||
|
||||
return [
|
||||
'has_error' => false,
|
||||
'error_code' => null,
|
||||
'error_message' => null,
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'niv' => $niv,
|
||||
'repuve_response' => $jsonResponse,
|
||||
];
|
||||
}
|
||||
|
||||
$errorFromDb = Error::where('code', '108')->first();
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => '108',
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'niv' => $niv,
|
||||
'repuve_response' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function verificarRobo(?string $niv = null, ?string $placa = null): array
|
||||
{
|
||||
try {
|
||||
$this->asegurarCargaCredenciales();
|
||||
|
||||
if (empty($niv) && empty($placa)) {
|
||||
logger()->warning('REPUVE verificarRobo: No se proporcionó NIV ni PLACA');
|
||||
return [
|
||||
'is_robado' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Debe proporcionar al menos NIV o PLACA para verificar robo',
|
||||
];
|
||||
}
|
||||
|
||||
$url = $this->baseUrl . $this->roboEndpoint;
|
||||
|
||||
// Construir arg2 según los parámetros enviados
|
||||
if (!empty($niv) && !empty($placa)) {
|
||||
$arg2 = $niv . '|' . $placa . str_repeat('|', 5);
|
||||
} elseif (!empty($niv)) {
|
||||
$arg2 = $niv . str_repeat('|', 7);
|
||||
} else {
|
||||
$arg2 = '||' . $placa . str_repeat('|', 5);
|
||||
}
|
||||
|
||||
logger()->info('REPUVE verificarRobo: Cadena construida', [
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
'arg2' => $arg2,
|
||||
'total_pipes' => substr_count($arg2, '|'),
|
||||
'ejemplo_visual' => str_replace('|', ' | ', $arg2),
|
||||
]);
|
||||
|
||||
|
||||
$soapBody = <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<wsdl:doConsRepRobo>
|
||||
<arg0>{$this->username}</arg0>
|
||||
<arg1>{$this->password}</arg1>
|
||||
<arg2>{$arg2}</arg2>
|
||||
</wsdl:doConsRepRobo>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: "doConsRepRobo"',
|
||||
'Content-Length: ' . strlen($soapBody),
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
// Si hay error de conexión, retornar error
|
||||
if ($error) {
|
||||
logger()->error('REPUVE verificarRobo: Error de conexión', [
|
||||
'error' => $error,
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
]);
|
||||
return [
|
||||
'is_robado' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Error de conexión con el servicio REPUVE',
|
||||
];
|
||||
}
|
||||
|
||||
// Si hay error HTTP, retornar error
|
||||
if ($httpCode !== 200) {
|
||||
logger()->error('REPUVE verificarRobo: HTTP error', [
|
||||
'http_code' => $httpCode,
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
]);
|
||||
return [
|
||||
'is_robado' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => "Error HTTP {$httpCode} del servicio REPUVE",
|
||||
];
|
||||
}
|
||||
|
||||
// Parsear respuesta
|
||||
$valorBuscado = $niv ?: $placa ?: 'N/A';
|
||||
$resultado = $this->parseRoboResponse($response, $valorBuscado);
|
||||
|
||||
// Si hubo error al parsear, loguear pero retornar el resultado completo
|
||||
if ($resultado['has_error'] ?? false) {
|
||||
logger()->warning('REPUVE verificarRobo: Error al parsear respuesta', [
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
'error' => $resultado['error_message'] ?? 'Desconocido',
|
||||
]);
|
||||
}
|
||||
|
||||
// Retornar el array completo con toda la información
|
||||
return $resultado;
|
||||
} finally {
|
||||
unset($ch);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logger()->error('REPUVE verificarRobo: Excepción capturada', [
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
return [
|
||||
'is_robado' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Excepción capturada: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function consultarVehiculo(?string $niv = null, ?string $placa = null)
|
||||
{
|
||||
try {
|
||||
$this->asegurarCargaCredenciales();
|
||||
|
||||
$url = $this->baseUrl . '/jaxws-consultarpv/ConsultaRpv';
|
||||
|
||||
// Construir arg2: NIV||||||||
|
||||
if ($placa) {
|
||||
$arg2 = ($niv ?? '') . '|' . $placa . str_repeat('|', 5);
|
||||
} else {
|
||||
$arg2 = ($niv ?? '') . str_repeat('|', 7);
|
||||
}
|
||||
|
||||
$soapBody = <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<wsdl:doConsRPV>
|
||||
<arg0>{$this->username}</arg0>
|
||||
<arg1>{$this->password}</arg1>
|
||||
<arg2>{$arg2}</arg2>
|
||||
</wsdl:doConsRPV>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: "doConsRPV"',
|
||||
'Content-Length: ' . strlen($soapBody),
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
if ($error) {
|
||||
logger()->error('REPUVE consultarVehiculo: Error de conexión', [
|
||||
'error' => $error,
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
]);
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Error de conexión con el servicio REPUVE',
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
logger()->error('REPUVE consultarVehiculo: HTTP error', [
|
||||
'http_code' => $httpCode,
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
]);
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => "Error HTTP {$httpCode} del servicio REPUVE",
|
||||
];
|
||||
}
|
||||
|
||||
// Parsear respuesta
|
||||
$resultado = $this->parseConsultarVehiculoResponse($response);
|
||||
|
||||
if ($resultado['has_error'] ?? false) {
|
||||
logger()->warning('REPUVE consultarVehiculo: Error al parsear', [
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
'error' => $resultado['error_message'] ?? 'Desconocido',
|
||||
]);
|
||||
}
|
||||
|
||||
return $resultado;
|
||||
} finally {
|
||||
unset($ch);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logger()->error('REPUVE consultarVehiculo: Excepción', [
|
||||
'niv' => $niv,
|
||||
'placa' => $placa,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Excepción: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function inscribirVehiculo(array $datos)
|
||||
{
|
||||
$this->asegurarCargaCredenciales();
|
||||
|
||||
$url = $this->baseUrl . $this->inscripcionEndpoint;
|
||||
|
||||
$arg2 = implode('|', [
|
||||
$datos['ent_fed'] ?? '', // 1. Entidad federativa
|
||||
$datos['ofcexp'] ?? '', // 2. Oficina expedición
|
||||
$datos['fechaexp'] ?? '', // 3. Fecha expedición
|
||||
$datos['placa'] ?? '', // 4. Placa
|
||||
$datos['tarjetacir'] ?? '', // 5. Tarjeta circulación
|
||||
$datos['marca'] ?? '', // 6. Marca
|
||||
$datos['submarca'] ?? '', // 7. Submarca
|
||||
$datos['version'] ?? '', // 8. Versión
|
||||
$datos['clase_veh'] ?? '', // 9. Clase vehículo
|
||||
$datos['tipo_veh'] ?? '', // 10. Tipo vehículo
|
||||
$datos['tipo_uso'] ?? '', // 11. Tipo uso
|
||||
$datos['modelo'] ?? '', // 12. Modelo (año)
|
||||
$datos['color'] ?? '', // 13. Color
|
||||
$datos['motor'] ?? '', // 14. Número motor
|
||||
$datos['niv'] ?? '', // 15. NIV
|
||||
$datos['rfv'] ?? '', // 16. RFV
|
||||
$datos['numptas'] ?? '', // 17. Número puertas
|
||||
$datos['observac'] ?? '', // 18. Observaciones
|
||||
$datos['tipopers'] ?? '', // 19. Tipo persona
|
||||
$datos['curp'] ?? '', // 20. CURP
|
||||
$datos['rfc'] ?? '', // 21. RFC
|
||||
$datos['pasaporte'] ?? '', // 22. Pasaporte
|
||||
$datos['licencia'] ?? '', // 23. Licencia
|
||||
$datos['nombre'] ?? '', // 24. Nombre
|
||||
$datos['ap_paterno'] ?? '', // 25. Apellido paterno
|
||||
$datos['ap_materno'] ?? '', // 26. Apellido materno
|
||||
$datos['ent_fed'] ?? '', // 27. Entidad federativa propietario
|
||||
$datos['munic'] ?? '', // 28. Municipio
|
||||
$datos['callep'] ?? '', // 29. Calle principal
|
||||
$datos['num_ext'] ?? '', // 30. Número exterior
|
||||
$datos['num_int'] ?? '', // 31. Número interior
|
||||
$datos['colonia'] ?? '', // 32. Colonia
|
||||
$datos['cp'] ?? '', // 33. Código postal
|
||||
$datos['cve_vehi'] ?? '', // 34. Clave vehículo
|
||||
$datos['nrpv'] ?? '', // 35. NRPV
|
||||
$datos['fe_act'] ?? '', // 36. Fecha actualización
|
||||
$datos['tipo_mov'] ?? '', // 37. Tipo movimiento
|
||||
]);
|
||||
|
||||
// Construir el cuerpo SOAP
|
||||
$soapBody = <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://inscripcion.org/wsdl">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<wsdl:inscribe>
|
||||
<arg0>{$this->username}</arg0>
|
||||
<arg1>{$this->password}</arg1>
|
||||
<arg2>{$arg2}</arg2>
|
||||
</wsdl:inscribe>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
|
||||
// Configurar cURL
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: "inscribe"',
|
||||
'Content-Length: ' . strlen($soapBody),
|
||||
]);
|
||||
|
||||
try {
|
||||
// Ejecutar la petición
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
// Loguear para debug
|
||||
logger()->info('REPUVE Inscripción Request', [
|
||||
'url' => $url,
|
||||
'soap_body' => $soapBody
|
||||
]);
|
||||
|
||||
logger()->info('REPUVE Inscripción Response', [
|
||||
'http_code' => $httpCode,
|
||||
'curl_error' => $curlError,
|
||||
'response' => $response
|
||||
]);
|
||||
|
||||
if ($curlError) {
|
||||
$errorFromDb = Error::where('code', '103')->first();
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => '103',
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? "Error de conexión: {$curlError}",
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'http_code' => $httpCode,
|
||||
'raw_response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$errorFromDb = Error::where('code', '-1')->first();
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => '-1',
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? "Error interno HTTP {$httpCode}",
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'http_code' => $httpCode,
|
||||
'raw_response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
// Parsear la respuesta
|
||||
return $this->parsearRespuestaInscripcion($response);
|
||||
} finally {
|
||||
unset($ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea la respuesta
|
||||
*/
|
||||
private function parsearRespuestaInscripcion(string $soapResponse)
|
||||
{
|
||||
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||
|
||||
if (!isset($matches[1])) {
|
||||
$errorFromDb = Error::where('code', '108')->first();
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => '108',
|
||||
'error_name' => $errorFromDb?->name,
|
||||
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'raw_response' => $soapResponse,
|
||||
'repuve_response' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$contenido = trim($matches[1]);
|
||||
|
||||
// Buscar patrones de error: ERR:, ERROR:, err:, error:
|
||||
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||
$errorCode = $errorMatch[2];
|
||||
|
||||
// Buscar el error completo en la base de datos
|
||||
$errorFromDb = Error::where('code', $errorCode)->first();
|
||||
|
||||
if ($errorFromDb) {
|
||||
// Retornar nombre y descripción de la BD
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => $errorCode,
|
||||
'error_name' => $errorFromDb->name,
|
||||
'error_message' => $errorFromDb->description,
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'raw_response' => $soapResponse,
|
||||
'repuve_response' => $contenido,
|
||||
];
|
||||
}
|
||||
|
||||
// Si no existe en BD, retornar el código sin descripción
|
||||
return [
|
||||
'has_error' => true,
|
||||
'error_code' => $errorCode,
|
||||
'error_name' => null,
|
||||
'error_message' => "Error código {$errorCode} - no catalogado",
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'raw_response' => $soapResponse,
|
||||
'repuve_response' => $contenido,
|
||||
];
|
||||
}
|
||||
|
||||
// Si empieza con OK: es éxito
|
||||
if (preg_match('/^OK:/i', $contenido)) {
|
||||
$datos = preg_replace('/^OK:/i', '', $contenido);
|
||||
|
||||
return [
|
||||
'has_error' => false,
|
||||
'error_code' => null,
|
||||
'error_message' => null,
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'raw_response' => $soapResponse,
|
||||
'repuve_response' => [
|
||||
'status' => 'OK',
|
||||
'data' => $datos,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Si no hay ERR/ERROR y no es OK, asumir que es respuesta exitosa
|
||||
return [
|
||||
'has_error' => false,
|
||||
'error_code' => null,
|
||||
'error_name' => null,
|
||||
'error_message' => null,
|
||||
'timestamp' => now()->toDateTimeString(),
|
||||
'raw_response' => $soapResponse,
|
||||
'repuve_response' => [
|
||||
'status' => 'OK',
|
||||
'data' => $contenido,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function parseRoboResponse(string $soapResponse, string $valor): array
|
||||
{
|
||||
// Extraer contenido del tag <return>
|
||||
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||
|
||||
if (!isset($matches[1])) {
|
||||
logger()->error('REPUVE parseRoboResponse: No se encontró tag <return>', [
|
||||
'soap_response' => substr($soapResponse, 0, 500),
|
||||
'valor' => $valor,
|
||||
]);
|
||||
|
||||
return [
|
||||
'has_error' => true,
|
||||
'is_robado' => false,
|
||||
'error_message' => 'Respuesta SOAP inválida',
|
||||
];
|
||||
}
|
||||
|
||||
$contenido = trim($matches[1]);
|
||||
|
||||
// Verificar si hay error de REPUVE Nacional (ERR: o ERROR:)
|
||||
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||
$errorCode = $errorMatch[2];
|
||||
|
||||
logger()->warning('REPUVE parseRoboResponse: Servicio retornó error', [
|
||||
'error_code' => $errorCode,
|
||||
'contenido' => $contenido,
|
||||
'valor' => $valor,
|
||||
]);
|
||||
|
||||
return [
|
||||
'has_error' => true,
|
||||
'is_robado' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => "Error REPUVE código {$errorCode}",
|
||||
'raw_response' => $contenido,
|
||||
];
|
||||
}
|
||||
|
||||
// Si empieza con OK:, parsear los datos de robo
|
||||
if (str_starts_with($contenido, 'OK:')) {
|
||||
$datos = str_replace('OK:', '', $contenido);
|
||||
$valores = explode('|', $datos);
|
||||
|
||||
// 1 = robado, 0 = no robado
|
||||
$indicador = $valores[0] ?? '0';
|
||||
$isRobado = ($indicador === '1');
|
||||
|
||||
$roboData = [
|
||||
'has_error' => false,
|
||||
'is_robado' => $isRobado,
|
||||
'indicador' => $indicador,
|
||||
'fecha_robo' => $valores[1] ?? null,
|
||||
'placa' => $valores[2] ?? null,
|
||||
'niv' => $valores[3] ?? null,
|
||||
'autoridad' => $valores[4] ?? null,
|
||||
'acta' => $valores[5] ?? null,
|
||||
'denunciante' => $valores[6] ?? null,
|
||||
'fecha_acta' => $valores[7] ?? null,
|
||||
'raw_response' => $contenido,
|
||||
];
|
||||
|
||||
// Log importante si está robado
|
||||
if ($isRobado) {
|
||||
logger()->warning('REPUVE: Vehículo reportado como ROBADO', [
|
||||
'valor_buscado' => $valor,
|
||||
'niv' => $roboData['niv'],
|
||||
'placa' => $roboData['placa'],
|
||||
'autoridad' => $roboData['autoridad'],
|
||||
'acta' => $roboData['acta'],
|
||||
'denunciante' => $roboData['denunciante'],
|
||||
]);
|
||||
} else {
|
||||
logger()->info('REPUVE: Vehículo NO reportado como robado', [
|
||||
'valor_buscado' => $valor,
|
||||
]);
|
||||
}
|
||||
|
||||
return $roboData;
|
||||
}
|
||||
|
||||
// Si no tiene formato reconocido
|
||||
logger()->error('REPUVE parseRoboResponse: Formato desconocido', [
|
||||
'contenido' => $contenido,
|
||||
'valor' => $valor,
|
||||
]);
|
||||
|
||||
return [
|
||||
'has_error' => true,
|
||||
'is_robado' => false,
|
||||
'error_message' => 'Formato de respuesta no reconocido',
|
||||
'raw_response' => $contenido,
|
||||
];
|
||||
}
|
||||
|
||||
public function parseConsultarVehiculoResponse(string $xmlResponse): array
|
||||
{
|
||||
try {
|
||||
$xml = simplexml_load_string($xmlResponse);
|
||||
|
||||
if ($xml === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Error al parsear XML',
|
||||
'raw_response' => $xmlResponse,
|
||||
];
|
||||
}
|
||||
|
||||
$xml->registerXPathNamespace('ns2', 'http://consultaRpv.org/wsdl');
|
||||
$return = $xml->xpath('//ns2:doConsRPVResponse/return');
|
||||
|
||||
if (empty($return)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'No se encontró elemento return en la respuesta',
|
||||
'raw_response' => $xmlResponse,
|
||||
];
|
||||
}
|
||||
$contenido = trim((string)$return[0]);
|
||||
|
||||
// Verificar si la respuesta es OK
|
||||
if (!str_starts_with($contenido, 'OK:')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => $contenido,
|
||||
'raw_response' => $xmlResponse,
|
||||
];
|
||||
}
|
||||
|
||||
// Remover "OK:" del inicio
|
||||
$data = substr($contenido, 3);
|
||||
|
||||
// Separar por |
|
||||
$campos = explode('|', $data);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'has_error' => false,
|
||||
'entidad_federativa' => $campos[0] ?? null,
|
||||
'oficina' => $campos[1] ?? null,
|
||||
'folio_tarjeta' => $campos[2] ?? null,
|
||||
'niv' => $campos[3] ?? null,
|
||||
'fecha_expedicion' => $campos[4] ?? null,
|
||||
'hora_expedicion' => $campos[5] ?? null,
|
||||
'procedencia' => $campos[6] ?? null,
|
||||
'origen' => $campos[7] ?? null,
|
||||
'clave_vehicular' => $campos[8] ?? null,
|
||||
'fecha_emplacado' => $campos[9] ?? null,
|
||||
'municipio' => $campos[10] ?? null,
|
||||
'serie' => $campos[11] ?? null,
|
||||
'placa' => $campos[12] ?? null,
|
||||
'tipo_vehiculo' => $campos[13] ?? null,
|
||||
'modelo' => $campos[14] ?? null,
|
||||
'color' => $campos[15] ?? null,
|
||||
'version' => $campos[16] ?? null,
|
||||
'entidad_placas' => $campos[17] ?? null,
|
||||
'marca' => $campos[18] ?? null,
|
||||
'linea' => $campos[19] ?? null,
|
||||
'uso' => $campos[20] ?? null,
|
||||
'clase' => $campos[21] ?? null,
|
||||
'estatus' => $campos[22] ?? null,
|
||||
'observaciones' => $campos[23] ?? null,
|
||||
'raw_response' => $contenido,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'has_error' => true,
|
||||
'error_message' => 'Excepción al parsear: ' . $e->getMessage(),
|
||||
'raw_response' => $xmlResponse,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,19 +7,13 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"barryvdh/laravel-dompdf": "*",
|
||||
"codedge/laravel-fpdf": "*",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/passport": "^12.4",
|
||||
"laravel/pulse": "^1.4",
|
||||
"laravel/reverb": "^1.4",
|
||||
"laravel/tinker": "^2.10",
|
||||
"milon/barcode": "^12.0",
|
||||
"notsoweb/laravel-core": "dev-main",
|
||||
"phpoffice/phpspreadsheet": "*",
|
||||
"setasign/fpdf": "^1.8",
|
||||
"spatie/laravel-permission": "^6.16",
|
||||
"spatie/simple-excel": "^3.8",
|
||||
"tightenco/ziggy": "^2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
2441
composer.lock
generated
2441
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -52,6 +52,7 @@
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'images' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/images'),
|
||||
|
||||
@ -35,16 +35,4 @@
|
||||
],
|
||||
],
|
||||
|
||||
'repuve_federal' => [
|
||||
'base_url' => env('REPUVE_FED_BASE_URL'),
|
||||
'robo_endpoint' => '/jaxws-consultarpv/ConsultaRpv',
|
||||
'inscripcion_endpoint' => '/jaxrpc-inscripcion/Inscripcion?WSDLs=',
|
||||
'username' => env('REPUVE_FED_USERNAME'),
|
||||
'password' => env('REPUVE_FED_PASSWORD'),
|
||||
],
|
||||
|
||||
'padron_estatal' => [
|
||||
'url' => env('REPUVE_EST_URL'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('modules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('municipality');
|
||||
$table->string('address');
|
||||
$table->string('colony');
|
||||
$table->string('cp')->nullable();
|
||||
$table->decimal('longitude', 10, 8)->nullable();
|
||||
$table->decimal('latitude', 10, 8)->nullable();
|
||||
$table->boolean('status')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('modules');
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('devices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('brand');
|
||||
$table->string('serie')->unique();
|
||||
$table->string('mac_address')->unique();
|
||||
$table->boolean('status')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('devices');
|
||||
}
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('owners', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('paternal')->nullable();
|
||||
$table->string('maternal')->nullable();
|
||||
$table->string('rfc')->unique()->nullable();
|
||||
$table->string('curp')->unique()->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->boolean('tipopers')->nullable();
|
||||
$table->string('pasaporte')->unique()->nullable();
|
||||
$table->string('licencia')->unique()->nullable();
|
||||
$table->string('ent_fed')->nullable();
|
||||
$table->string('munic')->nullable();
|
||||
$table->string('callep')->nullable();
|
||||
$table->string('num_ext')->nullable();
|
||||
$table->string('num_int')->nullable();
|
||||
$table->string('colonia')->nullable();
|
||||
$table->string('cp')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('owners');
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('errors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->text('name')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->text('type')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('errors');
|
||||
}
|
||||
};
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('device_module', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->foreignId('module_id')->constrained('modules')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->boolean('status')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['device_id', 'module_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('device_module');
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('lot');
|
||||
$table->string('box_number');
|
||||
$table->integer('starting_page');
|
||||
$table->integer('ending_page');
|
||||
$table->foreignId('module_id')->constrained('modules')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('packages');
|
||||
}
|
||||
};
|
||||
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('vehicle', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('placa')->unique()->nullable();
|
||||
$table->string('niv')->unique()->nullable();
|
||||
$table->string('marca')->nullable();
|
||||
$table->string('linea')->nullable();
|
||||
$table->string('sublinea')->nullable();
|
||||
$table->string('modelo')->nullable();
|
||||
$table->string('color')->nullable();
|
||||
$table->string('numero_motor')->nullable();
|
||||
$table->string('clase_veh')->nullable();
|
||||
$table->string('tipo_servicio')->nullable();
|
||||
$table->string('rfv')->unique()->nullable();
|
||||
$table->string('ofcexpedicion')->nullable();
|
||||
$table->date('fechaexpedicion')->nullable();
|
||||
$table->string('tipo_veh')->nullable();
|
||||
$table->string('numptas')->nullable();
|
||||
$table->string('observac')->nullable();
|
||||
$table->string('cve_vehi')->nullable();
|
||||
$table->string('tipo_mov')->nullable();
|
||||
$table->foreignId('owner_id')->nullable()->constrained('owners')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('vehicle');
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('folio')->unique();
|
||||
$table->string('tag_number')->unique()->nullable();
|
||||
$table->foreignId('vehicle_id')->nullable()->unique()->constrained('vehicle')->nullOnDelete();
|
||||
$table->foreignId('package_id')->nullable()->constrained('packages')->nullOnDelete();
|
||||
$table->enum('status', ['available', 'assigned', 'cancelled', 'lost'])->default('available');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tags');
|
||||
}
|
||||
};
|
||||
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('vehicle_tags_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('vehicle_id')->constrained('vehicle')->cascadeOnDelete();
|
||||
$table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
|
||||
$table->timestamp('cancellation_at')->nullable();
|
||||
$table->enum('cancellation_reason', [
|
||||
'fallo_lectura_handheld',
|
||||
'cambio_parabrisas',
|
||||
'roto_al_pegarlo',
|
||||
'extravio',
|
||||
'otro'
|
||||
])->nullable();
|
||||
$table->text('cancellation_observations')->nullable();
|
||||
$table->foreignId('cancelled_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('vehicle_tags_logs');
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('records', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('folio')->unique();
|
||||
$table->foreignId('vehicle_id')->constrained('vehicle')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('error_id')->nullable()->constrained('errors')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('records');
|
||||
}
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('files', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('path');
|
||||
$table->string('md5')->nullable();
|
||||
$table->foreignId('record_id')->constrained('records')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('files');
|
||||
}
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scan_history', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('scan_history');
|
||||
}
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('records', function (Blueprint $table) {
|
||||
$table->json('api_response')->nullable()->after('error_id');
|
||||
$table->timestamp('error_occurred_at')->nullable()->after('api_response');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('records', function (Blueprint $table) {
|
||||
$table->dropColumn(['api_response', 'error_occurred_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
$table->foreignId('responsible_id')
|
||||
->nullable()
|
||||
->after('name')
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
$table->dropForeign(['responsible_id']);
|
||||
$table->dropColumn('responsible_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->foreignId('module_id')
|
||||
->nullable()
|
||||
->after('email')
|
||||
->constrained('modules')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('catalog_name_img', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('catalog_name_img');
|
||||
}
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
<?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('files', function (Blueprint $table) {
|
||||
$table->foreignId('name_id')->after('id')->constrained('catalog_name_img')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->dropForeign(['name_id']);
|
||||
$table->dropColumn('name_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('vehicle', function (Blueprint $table) {
|
||||
$table->string('nrpv')->nullable()->after('cve_vehi');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('vehicle', function (Blueprint $table) {
|
||||
$table->dropColumn('nrpv');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('municipalities', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('municipalities');
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?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('modules', function (Blueprint $table) {
|
||||
// Eliminar la columna antigua
|
||||
$table->dropColumn('municipality');
|
||||
|
||||
// Agregar la nueva columna con foreign key
|
||||
$table->foreignId('municipality_id')
|
||||
->nullable()
|
||||
->constrained('municipalities')
|
||||
->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('modules', function (Blueprint $table) {
|
||||
$table->dropForeign(['municipality_id']);
|
||||
$table->dropColumn('municipality_id');
|
||||
$table->string('municipality');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('catalog_tag_status', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('records', function (Blueprint $table) {
|
||||
$table->foreignId('module_id')
|
||||
->nullable()
|
||||
->after('user_id')
|
||||
->constrained('modules')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('records', function (Blueprint $table) {
|
||||
$table->dropForeign(['module_id']);
|
||||
$table->dropColumn('module_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('owners', function (Blueprint $table) {
|
||||
$table->string('telefono')->nullable()->after('cp');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('owners', function (Blueprint $table) {
|
||||
$table->dropColumn('telefono');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
|
||||
$table->enum('action_type', [
|
||||
'inscripcion',
|
||||
'actualizacion',
|
||||
'sustitucion',
|
||||
'cancelacion'
|
||||
])->after('tag_id')->default('cancelacion');
|
||||
|
||||
$table->foreignId('performed_by')->nullable()->after('action_type')->constrained('users')->nullOnDelete();
|
||||
$table->index('action_type');
|
||||
$table->index('vehicle_id', 'action_type');
|
||||
$table->index('performed_by');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
|
||||
$table->dropIndex(['action_type']);
|
||||
$table->dropIndex(['vehicle_id', 'action_type']);
|
||||
$table->dropIndex(['performed_by']);
|
||||
|
||||
$table->dropForeign(['performed_by']);
|
||||
$table->dropColumn(['action_type', 'performed_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('catalog_cancellation_reasons', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->enum('applies_to', ['cancelacion', 'sustitucion', 'ambos']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('catalog_cancellation_reasons');
|
||||
}
|
||||
};
|
||||
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
|
||||
// Agregar nueva columna con foreign key
|
||||
$table->foreignId('cancellation_reason_id')
|
||||
->nullable()
|
||||
->after('tag_id')
|
||||
->constrained('catalog_cancellation_reasons')
|
||||
->nullOnDelete();
|
||||
|
||||
// Eliminar el enum viejo
|
||||
$table->dropColumn('cancellation_reason');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('vehicle_tags_logs', function (Blueprint $table) {
|
||||
$table->dropForeign(['cancellation_reason_id']);
|
||||
$table->dropColumn('cancellation_reason_id');
|
||||
|
||||
// Restaurar enum (solo si es necesario hacer rollback)
|
||||
$table->enum('cancellation_reason', [
|
||||
'fallo_lectura_handheld',
|
||||
'cambio_parabrisas',
|
||||
'roto_al_pegarlo',
|
||||
'extravio',
|
||||
'otro'
|
||||
])->nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tag_cancellation_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
|
||||
$table->foreignId('cancellation_reason_id')->nullable()->constrained('catalog_cancellation_reasons')->nullOnDelete();
|
||||
$table->text('cancellation_observations')->nullable();
|
||||
$table->timestamp('cancellation_at');
|
||||
$table->foreignId('cancelled_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tag_cancellation_logs');
|
||||
}
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('module_id')->nullable()->after('package_id');
|
||||
$table->foreign('module_id')->references('id')->on('modules')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->dropForeign(['module_id']);
|
||||
$table->dropColumn('module_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->dropForeign(['module_id']);
|
||||
$table->dropColumn(['box_number', 'starting_page', 'ending_page', 'module_id']);
|
||||
|
||||
$table->integer('total_boxes')->after('lot')->default(0);
|
||||
$table->string('description')->after('total_boxes')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->string('box_number')->after('lot');
|
||||
$table->integer('starting_page')->after('box_number');
|
||||
$table->integer('ending_page')->after('starting_page');
|
||||
$table->foreignId('module_id')->after('ending_page')
|
||||
->constrained('modules')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('boxes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('box_number');
|
||||
$table->foreignId('package_id')->constrained('packages')->cascadeOnDelete();
|
||||
$table->integer('starting_page');
|
||||
$table->integer('ending_page');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('boxes');
|
||||
}
|
||||
};
|
||||
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Obtener el nombre real de la FK si existe
|
||||
$foreignKeys = DB::select("
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'tags'
|
||||
AND COLUMN_NAME = 'package_id'
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
");
|
||||
|
||||
// Eliminar FK solo si existe
|
||||
if (!empty($foreignKeys)) {
|
||||
$constraintName = $foreignKeys[0]->CONSTRAINT_NAME;
|
||||
DB::statement("ALTER TABLE tags DROP FOREIGN KEY `{$constraintName}`");
|
||||
}
|
||||
|
||||
// Renombrar columna solo si existe package_id
|
||||
if (Schema::hasColumn('tags', 'package_id')) {
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('package_id', 'box_id');
|
||||
});
|
||||
}
|
||||
|
||||
$boxForeignKeys = DB::select("
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'tags'
|
||||
AND COLUMN_NAME = 'box_id'
|
||||
AND REFERENCED_TABLE_NAME = 'boxes'
|
||||
");
|
||||
|
||||
if (empty($boxForeignKeys)) {
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->foreign('box_id')->references('id')->on('boxes')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->dropForeign(['box_id']);
|
||||
$table->renameColumn('box_id', 'package_id');
|
||||
$table->foreign('package_id')->references('id')->on('packages')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,145 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// 1. Agregar columnas necesarias a packages (si no existen)
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('packages', 'box_number')) {
|
||||
$table->integer('box_number')->after('lot');
|
||||
}
|
||||
if (!Schema::hasColumn('packages', 'starting_page')) {
|
||||
$table->integer('starting_page')->after('box_number');
|
||||
}
|
||||
if (!Schema::hasColumn('packages', 'ending_page')) {
|
||||
$table->integer('ending_page')->after('starting_page');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Migrar datos de boxes a packages
|
||||
DB::table('boxes')->orderBy('id')->chunk(100, function($boxes) {
|
||||
foreach ($boxes as $box) {
|
||||
$parentPackage = DB::table('packages')->find($box->package_id);
|
||||
|
||||
$newPackageId = DB::table('packages')->insertGetId([
|
||||
'lot' => $parentPackage->lot,
|
||||
'box_number' => $box->box_number,
|
||||
'starting_page' => $box->starting_page,
|
||||
'ending_page' => $box->ending_page,
|
||||
'created_at' => $box->created_at,
|
||||
'updated_at' => $box->updated_at,
|
||||
]);
|
||||
|
||||
// Actualizar tags: box_id → package_id
|
||||
DB::table('tags')
|
||||
->where('box_id', $box->id)
|
||||
->update(['package_id' => $newPackageId]);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Agregar constraint único después de migrar datos (si no existe)
|
||||
if (!$this->constraintExists('packages', 'packages_lot_box_unique')) {
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->unique(['lot', 'box_number'], 'packages_lot_box_unique');
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Actualizar referencias en tags
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->dropForeign(['box_id']);
|
||||
});
|
||||
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('box_id', 'package_id');
|
||||
});
|
||||
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->foreign('package_id')->references('id')->on('packages')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 5. Eliminar tabla boxes
|
||||
Schema::dropIfExists('boxes');
|
||||
|
||||
// 6. Eliminar columnas de packages que ya no se usan
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('packages', 'total_boxes')) {
|
||||
$table->dropColumn('total_boxes');
|
||||
}
|
||||
if (Schema::hasColumn('packages', 'description')) {
|
||||
$table->dropColumn('description');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si existe un constraint/índice
|
||||
*/
|
||||
private function constraintExists(string $table, string $name): bool
|
||||
{
|
||||
$indexes = DB::select("SHOW INDEX FROM {$table}");
|
||||
foreach ($indexes as $index) {
|
||||
if ($index->Key_name === $name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Revertir cambios en orden inverso
|
||||
|
||||
// 1. Restaurar columnas de packages
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->integer('total_boxes')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
});
|
||||
|
||||
// 2. Recrear tabla boxes
|
||||
Schema::create('boxes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('package_id');
|
||||
$table->string('box_number');
|
||||
$table->integer('starting_page');
|
||||
$table->integer('ending_page');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('package_id')->references('id')->on('packages')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 3. Revertir columna package_id a box_id en tags
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->dropForeign(['package_id']);
|
||||
});
|
||||
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->renameColumn('package_id', 'box_id');
|
||||
});
|
||||
|
||||
Schema::table('tags', function (Blueprint $table) {
|
||||
$table->foreign('box_id')->references('id')->on('boxes')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 4. Eliminar constraint único de packages
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->dropUnique('packages_lot_box_unique');
|
||||
});
|
||||
|
||||
// 5. Eliminar columnas agregadas a packages
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->dropColumn(['box_number', 'starting_page', 'ending_page']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('vehicle', function (Blueprint $table) {
|
||||
$table->boolean('reporte_robo')->default(false)->after('placa');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('vehicle', function (Blueprint $table) {
|
||||
$table->dropColumn('reporte_robo');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->nullable()->after('ending_page');
|
||||
|
||||
// Foreign key constraint
|
||||
$table->foreign('user_id')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('packages', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->dropColumn('user_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->text('observations')->nullable()->after('md5');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->dropColumn('observations');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('users', 'username')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('username')->nullable()->unique()->after('maternal');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('username');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,70 +0,0 @@
|
||||
<?php namespace Database\Seeders;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Models\CatalogCancellationReason;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CatalogCancellationReasonSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Ejecutar sembrado de base de datos
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$reasons = [
|
||||
[
|
||||
'code' => '01',
|
||||
'name' => 'Fallo de lectura en handheld',
|
||||
'description' => 'El dispositivo handheld no puede leer el TAG correctamente',
|
||||
'applies_to' => 'ambos',
|
||||
],
|
||||
[
|
||||
'code' => '02',
|
||||
'name' => 'Cambio de parabrisas',
|
||||
'description' => 'El vehículo requirió cambio de parabrisas',
|
||||
'applies_to' => 'sustitucion',
|
||||
],
|
||||
[
|
||||
'code' => '03',
|
||||
'name' => 'TAG roto al pegarlo',
|
||||
'description' => 'El TAG se dañó durante la instalación',
|
||||
'applies_to' => 'sustitucion',
|
||||
],
|
||||
[
|
||||
'code' => '04',
|
||||
'name' => 'Extravío del TAG',
|
||||
'description' => 'El TAG fue extraviado o perdido',
|
||||
'applies_to' => 'ambos',
|
||||
],
|
||||
[
|
||||
'code' => '05',
|
||||
'name' => 'Daño físico del TAG',
|
||||
'description' => 'El TAG presenta daño físico que impide su funcionamiento',
|
||||
'applies_to' => 'ambos',
|
||||
],
|
||||
[
|
||||
'code' => '06',
|
||||
'name' => 'Otro motivo',
|
||||
'description' => 'Motivo no especificado en las opciones anteriores',
|
||||
'applies_to' => 'ambos',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($reasons as $reason) {
|
||||
CatalogCancellationReason::updateOrCreate(
|
||||
['code' => $reason['code']],
|
||||
$reason
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user