Compare commits

...

137 Commits

Author SHA1 Message Date
d6bf6b42ed Generar excel 2026-01-15 12:38:10 -06:00
Juan Felipe Zapata Moreno
7c530595f4 fix: seeder catalogo nombres de img 2026-01-14 15:49:01 -06:00
Juan Felipe Zapata Moreno
46367238f5 add: login con username 2026-01-14 15:08:54 -06:00
Juan Felipe Zapata Moreno
2ca6950751 add: historial de tags de un vehiculo 2026-01-14 12:45:13 -06:00
Juan Felipe Zapata Moreno
4f9b4a6098 fix: seeders direccion de modulos y catalogo de imagenes 2026-01-14 10:32:37 -06:00
Juan Felipe Zapata Moreno
e9fd55aa3b add: filtro por tag_number en consultaV 2025-12-29 13:41:10 -06:00
Juan Felipe Zapata Moreno
30c0b8f587 fix: cancelar constancia no asignada 2025-12-26 17:05:59 -06:00
Juan Felipe Zapata Moreno
d3bd94e158 fix: cancelación de constancia pdf dañado 2025-12-26 16:45:06 -06:00
Juan Felipe Zapata Moreno
1b2522c4f0 fix: cancelar constancia no asignada arreglado 2025-12-26 15:56:36 -06:00
Juan Felipe Zapata Moreno
3bde46589b fix: editar tag_number 2025-12-26 12:12:21 -06:00
Juan Felipe Zapata Moreno
88ef9d272c fix: tags caracteres 2025-12-24 09:46:58 -06:00
Juan Felipe Zapata Moreno
44ef2f9306 feat: actualizar método de cancelación de constancias para incluir validaciones y soporte de sustitución 2025-12-22 14:57:46 -06:00
Juan Felipe Zapata Moreno
908ba8aaf1 feat: agregar manejo de archivos duplicados en la actualización de datos del vehículo 2025-12-20 11:39:13 -06:00
Juan Felipe Zapata Moreno
a18f028a3d feat: generación de PDF para tags cancelados 2025-12-20 09:55:23 -06:00
Juan Felipe Zapata Moreno
672b7dd735 feat: agregar soporte para la sustitución de tags en la búsqueda de registros de vehículos, correción excel general 2025-12-18 16:02:03 -06:00
Juan Felipe Zapata Moreno
525bcc0db7 feat: implementar generación de PDF para tags dañados y modificaciones al actualizar datos del vehiculo 2025-12-18 12:54:02 -06:00
1c6cb99187 fix: mejorar validaciones y extraer datos adicionales en el proceso de cancelación de tags 2025-12-17 23:19:53 -06:00
89989c6fe4 MOD: Diseño de los excel corregidos 2025-12-17 22:53:50 -06:00
Juan Felipe Zapata Moreno
44d568e6c1 feat: filtrar tags por module_id 2025-12-12 09:10:58 -06:00
Juan Felipe Zapata Moreno
0741d1830f fix: updateData eliminar files 2025-12-11 16:54:01 -06:00
Juan Felipe Zapata Moreno
0ec75bc0ba fix: pdf tag sustituido ahora con id del record 2025-12-11 14:39:48 -06:00
c4643c60d4 feat: ajustar estilos en las plantillas de constancia, formulario y verificación 2025-12-10 21:52:50 -06:00
685ad3d3a8 feat: agregar logging y manejo de eliminación de archivos en PackageController y UpdateController 2025-12-10 20:52:45 -06:00
Juan Felipe Zapata Moreno
859596d858 hotfix: searchRecord name_id 2025-12-10 13:52:24 -06:00
Juan Felipe Zapata Moreno
8865000919 Fix: paquetes 2025-12-10 13:26:19 -06:00
Juan Felipe Zapata Moreno
20a7e3beda feat: agregar logging en el controlador de paquetes y ajustar estilos en la plantilla de constancia 2025-12-10 10:07:04 -06:00
Juan Felipe Zapata Moreno
18e3bdefdb Add: encriptar credenciales repuve 2025-12-10 09:24:54 -06:00
ebc4f3c546 feat: mejorar la asignación de tags a módulos y validaciones en el proceso 2025-12-09 19:21:49 -06:00
901957ff66 feat: actualizar para tags y filtro fecha de rango para consultaV 2025-12-09 17:41:29 -06:00
Juan Felipe Zapata Moreno
b68d360274 fix: correción de archivos 2025-12-09 16:53:54 -06:00
Juan Felipe Zapata Moreno
dcac63f953 Fix: error searchRecord 2025-12-08 12:58:45 -06:00
Juan Felipe Zapata Moreno
a8f84783cd fix: municipio pdf constancia 2025-12-08 12:30:37 -06:00
Juan Felipe Zapata Moreno
fd44d9c310 fix: municipio pdf constancia 2025-12-08 12:24:49 -06:00
Juan Felipe Zapata Moreno
0b498fb33f fix: contancia 2025-12-08 12:15:50 -06:00
Juan Felipe Zapata Moreno
ffed4cbde5 Fix: agregar validación para registros vacíos en Inscription, Package y Tags 2025-12-08 11:48:42 -06:00
Juan Felipe Zapata Moreno
6425aa5cc1 Fix: pdf tag asignados 2025-12-08 10:38:32 -06:00
Juan Felipe Zapata Moreno
ffc0fd1fd9 Fix: RepuveService consultar vehiculo 2025-12-08 10:18:43 -06:00
Juan Felipe Zapata Moreno
ff7b4a7d3d Fix: RepuveService consultar vehiculo 2025-12-08 10:14:21 -06:00
Juan Felipe Zapata Moreno
456768e1b5 Fix: vehicleUpdate error repuve 2025-12-08 10:06:00 -06:00
Juan Felipe Zapata Moreno
4004585dc8 Fix: vehicleUpdate 2025-12-08 09:53:46 -06:00
Juan Felipe Zapata Moreno
2db309a203 Fix: actualización auto 2025-12-08 09:38:15 -06:00
e0649e85ef fix: UpdateController 2025-12-08 02:44:33 -06:00
5a144e282f refactor: renombrar ruta de actualización de vehículo a 'actualizar-informacion' 2025-12-07 17:16:39 -06:00
Juan Felipe Zapata Moreno
3dee92ab1e Se quitó la validación de robo 2025-12-06 14:21:28 -06:00
Juan Felipe Zapata Moreno
ca5b1ad8ed fix: error vehiculo robo 2025-12-06 14:09:34 -06:00
Juan Felipe Zapata Moreno
1784e3065b Fix: pacakgeController store error tags 2025-12-06 13:40:09 -06:00
Juan Felipe Zapata Moreno
553752fcfc fix: InscriptionController checkIfStolen 2025-12-06 13:37:06 -06:00
Juan Felipe Zapata Moreno
c56e3b1435 REFACTOR: Mejorar la lógica de filtrado en ExcelController y optimizar la creación de tags en PackageController 2025-12-06 12:55:00 -06:00
Juan Felipe Zapata Moreno
74dedc32df ADD: Implementar reporte de robo en vehículos y actualizar lógica de consulta 2025-12-06 11:51:34 -06:00
Juan Felipe Zapata Moreno
118f5ef868 Correción servicio repuve, endpoint stolen 2025-12-06 10:05:31 -06:00
baf3961036 REFACTOR: Eliminar el modelo y controlador de cajas, actualizar relaciones en paquetes y etiquetas 2025-12-05 23:04:20 -06:00
8c6afe40bc ADD: Implementar generación de PDF para tags sustituidos, correción de Reporte Robo 2025-12-05 21:55:00 -06:00
Juan Felipe Zapata Moreno
cb16c0b91c Añadir ruta para obtener constancias generales en Excel y mejorar formato de fechas en constancias sustituidas y canceladas 2025-12-05 16:49:19 -06:00
Juan Felipe Zapata Moreno
5826a2c26c Correción en BackupCron para asegurar la creación del directorio de backup y manejo de errores 2025-12-05 12:58:37 -06:00
Juan Felipe Zapata Moreno
0be583a088 Correción Excel Canceladas, Correción Search Record, Creación del Actualización (tagSubstitution) 2025-12-05 11:27:08 -06:00
Juan Felipe Zapata Moreno
96f938a279 Correción vehicle consultaV 2025-12-04 16:34:44 -06:00
Juan Felipe Zapata Moreno
dd2298a5c8 Correción PadronEstatalService nrpv 2025-12-04 16:23:49 -06:00
Juan Felipe Zapata Moreno
d9bc4886ce VehicleStoreRequest correción 2025-12-04 16:15:35 -06:00
Juan Felipe Zapata Moreno
3079826f0f Corrección tag_number nullable en TagsController 2025-12-04 16:09:57 -06:00
Juan Felipe Zapata Moreno
3e48f8d6db Correción modulos 2025-12-04 15:59:49 -06:00
Juan Felipe Zapata Moreno
df252f55f2 Modificación a la migración tags 2025-12-04 15:32:05 -06:00
Juan Felipe Zapata Moreno
b5779e5bdb ADD: Se actualizó la lógica de cancelación de constancias y se mejoró la validación de tags en CancellationController e InscriptionController. Se modificó la validación de tag_number en TagsController para permitir valores nulos. 2025-12-04 15:00:42 -06:00
Juan Felipe Zapata Moreno
8e2db75ad2 ADD: Se mejoró la búsqueda de registros en InscriptionController y se añadieron nuevos filtros. Se actualizó la generación de PDF en RecordController para incluir datos del vehículo y propietario. Se modificó la ruta para generar el formulario PDF. 2025-12-04 13:50:46 -06:00
Juan Felipe Zapata Moreno
70f3679ba4 ADD: Se implementó el controlador de cajas y se añadieron las rutas correspondientes, junto con las solicitudes de validación para el almacenamiento y actualización de cajas. 2025-12-04 11:51:56 -06:00
Juan Felipe Zapata Moreno
6af33e4503 ADD: Se implementó el comando para realizar respaldos automáticos de la base de datos 2025-12-03 13:09:47 -06:00
02220407ff ADD: Se agregó el endpoint para obtener constancias canceladas y se implementó la generación de archivos Excel correspondientes. 2025-12-02 21:15:37 -06:00
Juan Felipe Zapata Moreno
80c0076a92 Fix: searchRecord vehicle y vehicle.owner 2025-12-02 14:25:38 -06:00
Juan Felipe Zapata Moreno
9b37989f0a fix: nrpv 2025-12-02 13:12:53 -06:00
Juan Felipe Zapata Moreno
5efce1978b Se implementó reenviar a repuve 2025-12-01 15:56:01 -06:00
Juan Felipe Zapata Moreno
fbf55ae67c endpoint para actualizar web 2025-12-01 13:45:07 -06:00
Juan Felipe Zapata Moreno
48eaf28151 Se implementó la queue a UpdateController 2025-12-01 12:17:11 -06:00
Juan Felipe Zapata Moreno
b25db1a0d4 Logs al job de repuve nacional 2025-12-01 11:41:39 -06:00
Juan Felipe Zapata Moreno
ac57bdcbc4 Queue al servicio de repuve 2025-12-01 10:03:49 -06:00
92b64887bd fix: Mejorar el manejo de errores en la respuesta del servicio REPUVE 2025-11-29 12:49:24 -06:00
9ebc3f4167 Cambio se agregó busqueda por placa la busqueda repuve estatal 2025-11-29 12:26:10 -06:00
3d6649c504 Correción validación 2025-11-29 12:10:26 -06:00
c9e5cb86c8 Cambios a inscripcion, placa en vez de niv y correción de fecha en PadronEstatalService 2025-11-29 12:02:56 -06:00
fd727ca45f Correción mensaje de error al crear muchos tags 2025-11-29 11:28:18 -06:00
36865e7cef fix: Mejorar la validación y asignación de tags en el controlador de Tags 2025-11-29 10:54:44 -06:00
75889becaf Correciones a RepuveService, Tags y Module 2025-11-29 02:49:51 -06:00
Juan Felipe Zapata Moreno
d8ec98cd7c feat: Agregar validaciones de longitud para 'niv' y 'placa' en el formulario PDF, array de tags 2025-11-28 17:01:54 -06:00
2d060f9909 feat: Mejorar la validación de errores en la cancelación de constancias y optimizar la búsqueda de tags por lote 2025-11-27 22:33:21 -06:00
Juan Felipe Zapata Moreno
cf7cfdb821 feat: Agregar campos de marca y línea en la generación del formulario PDF 2025-11-27 18:11:54 -06:00
Juan Felipe Zapata Moreno
ebc64a6a8e fix: Corregir la vista del PDF en la generación del formulario 2025-11-27 18:09:26 -06:00
Juan Felipe Zapata Moreno
684315eb18 feat: Implementación catalogo de razones de cancelación, actualizar manual, pdf contancia dañada 2025-11-27 17:23:04 -06:00
Juan Felipe Zapata Moreno
a305c82956 feat: Actualizar la lógica de cancelación para usar el folio correcto 2025-11-26 17:22:45 -06:00
Juan Felipe Zapata Moreno
e57bb79762 Correcion ruta excel 2025-11-26 16:50:22 -06:00
Juan Felipe Zapata Moreno
f25901ed9d Fix: Correciones de cancelacion y logs, creación del excel tag sustituidos 2025-11-26 16:49:24 -06:00
Juan Felipe Zapata Moreno
975c6863ff feat: Agregar campo 'telefono' a la inscripción de vehículos y actualizar las reglas de validación en VehicleStoreRequest 2025-11-26 10:23:51 -06:00
dfb60806cb feat: Add module_id to records and telefono to owners, implement PDF form generation, and update PDF views for better layout and data presentation. 2025-11-25 22:09:37 -06:00
90f943291e feat: Implement notification controller, add new image, configure Git ignores for storage and cache, and update Dockerfile to Alpine. 2025-11-24 17:10:45 -06:00
Juan Felipe Zapata Moreno
6388410153 Correcion cancellationController 2025-11-22 14:42:46 -06:00
Juan Felipe Zapata Moreno
cac2263a4f DatabaseSeeder error 2025-11-22 14:27:31 -06:00
Juan Felipe Zapata Moreno
c7f1b46714 manejo de errores 2025-11-22 14:25:11 -06:00
Juan Felipe Zapata Moreno
78ac5ab75e FEAT: implementar catálogo de estatus de tags y actualizar update y cancellation controller 2025-11-22 12:14:53 -06:00
ef864c4753 Correciones a Actualizar y Cancelar 2025-11-21 20:17:26 -06:00
Juan Felipe Zapata Moreno
4c6dccf056 FIX: eliminar dato hardcodeado niv 2025-11-21 17:01:08 -06:00
Juan Felipe Zapata Moreno
48fec8fdd8 FIX: Actualizar consulta de padrón vehicular y datos de inscripción 2025-11-21 16:47:01 -06:00
Juan Felipe Zapata Moreno
f05e1679a0 FIX 2025-11-21 16:10:51 -06:00
Juan Felipe Zapata Moreno
ee7947ae29 Correciones repuve 2025-11-21 13:48:03 -06:00
Juan Felipe Zapata Moreno
3640fc8a13 ADD: Implementar servicios para consulta de REPUVE 2025-11-21 12:33:50 -06:00
Juan Felipe Zapata Moreno
1060435f52 FIX: Actualizar referencias de número de serie a NIV en documentos PDF 2025-11-21 10:39:27 -06:00
Juan Felipe Zapata Moreno
d995e27a39 FIX: Filtrar roles y usuarios para excluir 'developer' 2025-11-20 17:19:14 -06:00
Juan Felipe Zapata Moreno
16361e0a27 FIX: Actualización de correos UserSeeder 2025-11-15 13:25:05 -06:00
Juan Felipe Zapata Moreno
12986d51cc ADD: Método para cambiar el estado de un dispositivo y ruta correspondiente 2025-11-15 12:16:19 -06:00
Juan Felipe Zapata Moreno
27cbc965b5 FIX: Correción modulos y asignación de permisos 2025-11-15 10:07:59 -06:00
Juan Felipe Zapata Moreno
82b7a179f5 FIX: Correcciones device y modulo 2025-11-14 11:44:07 -06:00
Juan Felipe Zapata Moreno
551ef38ffc ADD: Método show a devices, packages y modules 2025-11-05 10:48:20 -06:00
Juan Felipe Zapata Moreno
9348e03010 Merge branch 'develop' of git.golsystems.mx:juan.zapata/repuve-backend-v1 into develop 2025-11-04 12:04:55 -06:00
Juan Felipe Zapata Moreno
9a4f70baa2 Arreglo de la paginación y actualización de expediente 2025-11-04 12:01:02 -06:00
83065b1cc4 fix: DevSeeder 2025-11-04 09:30:10 -06:00
Juan Felipe Zapata Moreno
dd2b3211db FIX: relación municipality con modules 2025-11-03 16:28:37 -06:00
Juan Felipe Zapata Moreno
7a5bf3a2a0 ADD: municipality 2025-11-03 15:43:12 -06:00
Juan Felipe Zapata Moreno
fae8979532 Ultimos cambios - endpoint stolen 2025-11-03 11:19:12 -06:00
Juan Felipe Zapata Moreno
7ad6f87b17 ADD: nrpv a vehicle y paginación a usuarios 2025-10-30 18:02:20 -06:00
Juan Felipe Zapata Moreno
906299ac10 ADD: Nueva tabla catalogo, seeders catalog y modulos 2025-10-30 17:33:05 -06:00
Juan Felipe Zapata Moreno
d7761088dd Arreglo a imagenes y al searchRecord 2025-10-29 15:58:07 -06:00
Juan Felipe Zapata Moreno
41c85a8ade UPDATE: Se actualizó el método de actualizar inscripción 2025-10-29 12:36:12 -06:00
Juan Felipe Zapata Moreno
3a25257dc5 WIP 2025-10-28 16:50:40 -06:00
1d69c5ee7c Conflictos resueltos 2025-10-27 19:29:03 -06:00
Juan Felipe Zapata Moreno
99c2767877 ADD: TagNumber 2025-10-27 16:42:40 -06:00
843d3404e9 Update Tag Model, add TagController and routes, fix InscriptionControlle 2025-10-27 09:25:30 -06:00
Juan Felipe Zapata Moreno
e97d124967 ADD: Controller paquetes de tags 2025-10-24 16:50:07 -06:00
Juan Felipe Zapata Moreno
2fc21be5a2 WIP listar devices y creación agregado 2025-10-23 17:01:49 -06:00
Juan Felipe Zapata Moreno
c4935d5298 WIP - correción de la respuesta de consulta vehiculos 2025-10-23 11:55:38 -06:00
beeaf481a0 ADD: Rutas para usuarios y roles 2025-10-22 23:34:03 -06:00
Juan Felipe Zapata Moreno
aa2266f34d ADD: Se crearon las api de modulos y correción a inscripción y actualizacion de constancia 2025-10-22 16:56:59 -06:00
Juan Felipe Zapata Moreno
d281e505e0 ADD: Cancelación, pdfs agregados 2025-10-21 17:23:54 -06:00
bf0346dabf ADD: Proceso de Cancelación 2025-10-20 21:47:44 -06:00
Juan Felipe Zapata Moreno
de0ac7a3aa FEAT: Creacion de RecordController, FileStoreRequest, actualiza modelos y controladores, renombra VehicleRequest, y ajusta configuración y vistas para gestión de expedientes 2025-10-20 16:29:31 -06:00
71788ddd2e ADD: api consulta 2025-10-19 21:15:35 -06:00
f3dd6cd32c FIX: Orden de migraciones 2025-10-19 18:52:25 -06:00
4e1c5855af ADD: Se crearon las migraciones y los modelos 2025-10-19 18:25:48 -06:00
Juan Felipe Zapata Moreno
92bf244fe6 ADD: Creación de Migraciones 2025-10-18 14:15:09 -06:00
Juan Felipe Zapata Moreno
a66b8d77d6 ADD: docker compose modificado se agregó mongo 2025-10-18 13:51:40 -06:00
Juan Felipe Zapata Moreno
2b448644c2 ADD: Simulación Robo 2025-10-17 10:33:14 -06:00
Juan Felipe Zapata Moreno
2bb87e7daf ADD: Implementación de MongoDB y conexión con compass 2025-10-16 15:55:19 -06:00
128 changed files with 15187 additions and 3713 deletions

View File

@ -36,7 +36,6 @@ DB_USERNAME=notsoweb
DB_PASSWORD= DB_PASSWORD=
PMA_PORT=8081 # Puerto para phpMyAdmin PMA_PORT=8081 # Puerto para phpMyAdmin
REDIS_PORT=6379 # Puerto para Redis
NGINX_PORT=8080 # Puerto para Nginx NGINX_PORT=8080 # Puerto para Nginx
SESSION_DRIVER=database SESSION_DRIVER=database
@ -75,6 +74,14 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET= AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false 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_ID=
REVERB_APP_KEY= REVERB_APP_KEY=
REVERB_APP_SECRET= REVERB_APP_SECRET=

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
/public/vendor /public/vendor
/storage/*.key /storage/*.key
/storage/pail /storage/pail
/storage/app/backup
/vendor /vendor
.env .env
.env.backup .env.backup

View File

@ -1,7 +1,7 @@
server { server {
listen 80; listen 80;
server_name _; server_name _;
root /var/www/golscontrols/public; root /var/www/repuve-backend-v1/public;
index index.php index.html; index index.php index.html;
# Logging # Logging
@ -17,7 +17,7 @@ server {
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass golscontrols:9000; fastcgi_pass repuve-backend:9000;
fastcgi_index index.php; fastcgi_index index.php;
# Timeouts importantes para evitar errores 500 # Timeouts importantes para evitar errores 500
@ -45,17 +45,17 @@ server {
# Handle storage files (Laravel storage link) # Handle storage files (Laravel storage link)
location /storage { location /storage {
alias /var/www/golscontrols/storage/app; alias /var/www/repuve-backend-v1/storage/app/public;
try_files $uri =404; try_files $uri =404;
} }
location /profile { location /profile {
alias /var/www/golscontrols/storage/app/profile; alias /var/www/repuve-backend-v1/storage/app/profile;
try_files $uri =404; try_files $uri =404;
} }
location /images { location /images {
alias /var/www/golscontrols/storage/app/images; alias /var/www/repuve-backend-v1/storage/app/images;
try_files $uri =404; try_files $uri =404;
} }

View File

@ -0,0 +1,95 @@
<?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];
}
}

View File

@ -0,0 +1,140 @@
<?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;
}
}
}

View File

@ -0,0 +1,59 @@
<?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;
}
}

View File

@ -26,7 +26,7 @@ class RoleController extends Controller
*/ */
public function index() public function index()
{ {
$model = Role::orderBy('description'); $model = Role::where('name', '!=','developer')->orderBy('description');
QuerySupport::queryByKey($model, request(), 'name'); QuerySupport::queryByKey($model, request(), 'name');
@ -41,9 +41,13 @@ public function index()
*/ */
public function store(RoleStoreRequest $request) public function store(RoleStoreRequest $request)
{ {
Role::create($request->all()); $model = Role::create($request->all());
return ApiResponse::OK->response(); return ApiResponse::OK->response([
'message' => 'Rol creado exitosamente',
'id' => $model->id,
'name' => $model->name,
]);
} }
/** /**

View File

@ -29,12 +29,23 @@ class UserController extends Controller
*/ */
public function index() public function index()
{ {
$users = User::orderBy('name'); $users = User::whereDoesntHave('roles', function ($query) {
$query->where('name', 'developer');
})->orderBy('name');
QuerySupport::queryByKeys($users, ['name', 'email']); QuerySupport::queryByKeys($users, ['name', 'email']);
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'models' => $users->paginate(config('app.pagination')) /* 'models' => $users->paginate(config('app.pagination')), */
'users' => $users->select([
'id',
'name',
'paternal',
'maternal',
'email',
'module_id',
'deleted_at'
])->paginate(config('app.pagination'))
]); ]);
} }
@ -49,7 +60,10 @@ public function store(UserStoreRequest $request)
$user->roles()->sync($request->roles); $user->roles()->sync($request->roles);
} }
return ApiResponse::OK->response(); return ApiResponse::OK->response([
'message' => 'Usuario actualizado exitosamente',
'user' => $user->load(['module', 'roles']),
]);
} }
/** /**
@ -69,7 +83,14 @@ public function update(UserUpdateRequest $request, User $user)
{ {
$user->update($request->all()); $user->update($request->all());
return ApiResponse::OK->response(); if ($request->has('roles')) {
$user->roles()->sync($request->roles);
}
return ApiResponse::OK->response([
'message' => 'Usuario actualizado exitosamente',
'user' => $user->load(['module', 'roles']),
]);
} }
/** /**

View File

@ -0,0 +1,383 @@
<?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(),
]);
}
}
}

View File

@ -0,0 +1,115 @@
<?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',
]);
}
}

View File

@ -0,0 +1,76 @@
<?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(),
]);
}
}
}

View File

@ -0,0 +1,230 @@
<?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

View File

@ -0,0 +1,606 @@
<?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(),
]);
}
}
}

View File

@ -0,0 +1,258 @@
<?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(),
]);
}
}
}

View File

@ -0,0 +1,72 @@
<?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();
}
}

View File

@ -0,0 +1,485 @@
<?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(),
]);
}
}
}

View File

@ -0,0 +1,563 @@
<?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')),
]);
}
}

View File

@ -0,0 +1,589 @@
<?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

View File

@ -25,14 +25,20 @@ class LoginController extends Controller
{ {
/** /**
* Iniciar sesión * Iniciar sesión
* Permite login con username O email
*/ */
public function login(LoginRequest $request) public function login(LoginRequest $request)
{ {
$user = User::where('email', $request->get('email'))->first(); $credential = $request->get('username');
// Buscar por username o email
$user = User::where('username', $credential)
->orWhere('email', $credential)
->first();
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) { if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
return ApiResponse::UNPROCESSABLE_CONTENT->response([ return ApiResponse::UNPROCESSABLE_CONTENT->response([
'email' => ['Usuario no valido'] 'username' => ['Credenciales inválidas']
]); ]);
} }

View File

@ -0,0 +1,77 @@
<?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
]
]);
}
}

View File

@ -30,8 +30,17 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'email' => ['required', 'email'], 'username' => ['required', 'string'], // Acepta username o email
'password' => ['required', 'min:8'], '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',
];
}
} }

View File

@ -0,0 +1,57 @@
<?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',
];
}
}

View File

@ -0,0 +1,26 @@
<?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',
];
}
}

View File

@ -0,0 +1,26 @@
<?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',
];
}
}

View File

@ -0,0 +1,39 @@
<?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',
];
}
}

View File

@ -0,0 +1,37 @@
<?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',
];
}
}

View File

@ -0,0 +1,30 @@
<?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',
];
}
}

View File

@ -0,0 +1,51 @@
<?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',
];
}
}

View File

@ -0,0 +1,51 @@
<?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',
];
}
}

View File

@ -0,0 +1,39 @@
<?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',
];
}
}

View File

@ -0,0 +1,41 @@
<?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',
];
}
}

View File

@ -0,0 +1,50 @@
<?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)'
);
}
});
}
}

View File

@ -0,0 +1,43 @@
<?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',
];
}
}

View File

@ -0,0 +1,90 @@
<?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',
];
}
}

View File

@ -3,6 +3,7 @@
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved
*/ */
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
/** /**
@ -39,4 +40,21 @@ public function rules(): array
'roles' => ['nullable', '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')
)
]);
}
}
} }

View File

@ -0,0 +1,117 @@
<?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,
]);
}
}
}

View File

@ -0,0 +1,42 @@
<?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');
}
}

View File

@ -0,0 +1,28 @@
<?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');
}
}

View File

@ -0,0 +1,62 @@
<?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);
}
}

45
app/Models/Device.php Normal file
View File

@ -0,0 +1,45 @@
<?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();
}
}

View File

@ -0,0 +1,42 @@
<?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);
}
}

23
app/Models/Error.php Normal file
View File

@ -0,0 +1,23 @@
<?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);
}
}

43
app/Models/File.php Normal file
View File

@ -0,0 +1,43 @@
<?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),
);
}
}

71
app/Models/Module.php Normal file
View File

@ -0,0 +1,71 @@
<?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');
}
}

View File

@ -0,0 +1,27 @@
<?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);
}
}

53
app/Models/Owner.php Normal file
View File

@ -0,0 +1,53 @@
<?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');
}
}

34
app/Models/Package.php Normal file
View File

@ -0,0 +1,34 @@
<?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);
}
}

63
app/Models/Record.php Normal file
View File

@ -0,0 +1,63 @@
<?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'
);
}
}

View File

@ -0,0 +1,36 @@
<?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);
}
}

133
app/Models/Tag.php Normal file
View File

@ -0,0 +1,133 @@
<?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;
}
}

View File

@ -0,0 +1,51 @@
<?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');
}
}

View File

@ -1,4 +1,7 @@
<?php namespace App\Models; <?php
namespace App\Models;
/** /**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/ */
@ -42,10 +45,12 @@ class User extends Authenticatable
'name', 'name',
'paternal', 'paternal',
'maternal', 'maternal',
'username',
'email', 'email',
'phone', 'phone',
'password', 'password',
'profile_photo_path', 'profile_photo_path',
'module_id'
]; ];
/** /**
@ -98,7 +103,7 @@ public function reports()
public function fullName(): Attribute public function fullName(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal, get: fn() => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
); );
} }
@ -108,7 +113,7 @@ public function fullName(): Attribute
public function lastName(): Attribute public function lastName(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->paternal . ' ' . $this->maternal, get: fn() => $this->paternal . ' ' . $this->maternal,
); );
} }
@ -127,4 +132,103 @@ public function resetPasswords()
{ {
return $this->hasMany(ResetPassword::class); 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;
}
} }

62
app/Models/Vehicle.php Normal file
View File

@ -0,0 +1,62 @@
<?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);
}
}

View File

@ -0,0 +1,69 @@
<?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';
}
}

View File

@ -0,0 +1,223 @@
<?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,
];
}
}

View File

@ -0,0 +1,829 @@
<?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,
];
}
}
}

View File

@ -7,13 +7,19 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"barryvdh/laravel-dompdf": "*",
"codedge/laravel-fpdf": "*",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/passport": "^12.4", "laravel/passport": "^12.4",
"laravel/pulse": "^1.4", "laravel/pulse": "^1.4",
"laravel/reverb": "^1.4", "laravel/reverb": "^1.4",
"laravel/tinker": "^2.10", "laravel/tinker": "^2.10",
"milon/barcode": "^12.0",
"notsoweb/laravel-core": "dev-main", "notsoweb/laravel-core": "dev-main",
"phpoffice/phpspreadsheet": "*",
"setasign/fpdf": "^1.8",
"spatie/laravel-permission": "^6.16", "spatie/laravel-permission": "^6.16",
"spatie/simple-excel": "^3.8",
"tightenco/ziggy": "^2.5" "tightenco/ziggy": "^2.5"
}, },
"require-dev": { "require-dev": {

2457
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,6 @@
'visibility' => 'public', 'visibility' => 'public',
'throw' => false, 'throw' => false,
], ],
'images' => [ 'images' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app/images'), 'root' => storage_path('app/images'),

View File

@ -35,4 +35,16 @@
], ],
], ],
'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'),
],
]; ];

View File

@ -0,0 +1,35 @@
<?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');
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,43 @@
<?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');
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,33 @@
<?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');
}
};

View File

@ -0,0 +1,32 @@
<?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');
}
};

View File

@ -0,0 +1,46 @@
<?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');
}
};

View File

@ -0,0 +1,32 @@
<?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');
}
};

View File

@ -0,0 +1,39 @@
<?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');
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,30 @@
<?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');
}
};

View File

@ -0,0 +1,29 @@
<?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');
}
};

View File

@ -0,0 +1,29 @@
<?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']);
});
}
};

View File

@ -0,0 +1,33 @@
<?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');
});
}
};

View File

@ -0,0 +1,32 @@
<?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) {
//
});
}
};

View File

@ -0,0 +1,29 @@
<?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');
}
};

View File

@ -0,0 +1,23 @@
<?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');
});
}
};

View File

@ -0,0 +1,28 @@
<?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');
});
}
};

View File

@ -0,0 +1,29 @@
<?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');
}
};

View File

@ -0,0 +1,31 @@
<?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');
});
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,59 @@
<?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');
});
}
};

View File

@ -0,0 +1,33 @@
<?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');
});
}
};

View File

@ -0,0 +1,28 @@
<?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');
});
}
};

View File

@ -0,0 +1,43 @@
<?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']);
});
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,46 @@
<?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();
});
}
};

View File

@ -0,0 +1,32 @@
<?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');
}
};

View File

@ -0,0 +1,30 @@
<?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');
});
}
};

View File

@ -0,0 +1,37 @@
<?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();
});
}
};

View File

@ -0,0 +1,31 @@
<?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');
}
};

View File

@ -0,0 +1,65 @@
<?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');
});
}
};

View File

@ -0,0 +1,145 @@
<?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']);
});
}
};

View File

@ -0,0 +1,28 @@
<?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');
});
}
};

View File

@ -0,0 +1,35 @@
<?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');
});
}
};

View File

@ -0,0 +1,28 @@
<?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');
});
}
};

View File

@ -0,0 +1,30 @@
<?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');
});
}
};

View File

@ -0,0 +1,70 @@
<?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