Compare commits

..

225 Commits

Author SHA1 Message Date
e272b60c43 fix: mejorar lógica de filtrado y asignación de tags en constanciasSustituida 2026-04-03 15:18:17 -06:00
706625575c feat: cambiar tipo de datos de starting_page y ending_page a string en la tabla packages y actualizar validaciones en las solicitudes 2026-04-02 16:50:28 -06:00
73b0e6f1b1 fix: update MySQL log path in Alloy configuration and adjust Docker volumes for MySQL logs 2026-04-01 10:41:27 -06:00
3a7b5cfc47 feat: add MySQL logging configuration and update docker-compose for log persistence 2026-04-01 10:22:26 -06:00
ce67ba2e4c fix: docker compose de prod 2026-03-31 17:04:10 -06:00
b216258bdc fix: actualizar sustitucion para los tags de los vehiculos y corregir los campos del pdf 2026-03-31 16:47:10 -06:00
ea2dd1b3ff feat: add Grafana, Loki, and Alloy services to Docker configurations for Dev, Prod, and QA environments 2026-03-31 14:06:17 -06:00
d10b578c61 Fix: update MySQL port mapping to use DB_PORT_FORWARD variable 2026-03-31 12:56:09 -06:00
9303992795 Fix: update MySQL port mapping to use DB_PORT_FORWARD variable 2026-03-31 12:44:58 -06:00
2c7ab0cb2e feat: mejora lógica de inscripción de vehículos y logging
- Se actualizó la validación de asignación de módulo de tags.
- Se mejoró el registro de eventos (logging).
2026-03-31 12:30:19 -06:00
2fd337bd16 feat: add production environment configuration files for Docker setup 2026-03-30 18:53:31 -06:00
09e6262c43 feat: add production environment configuration files for Docker setup 2026-03-30 18:52:23 -06:00
f2a15ef113 feat: implement parallel SOAP requests for vehicle verification and add supervisor configuration 2026-03-30 14:51:01 -06:00
b17b6608d3 feat: update fechaexpedicion to string format and implement logging services 2026-03-30 12:14:57 -06:00
be300d449d feat: mejora validación de vehículos y manejo de datos REPUVE
- Se actualizó la validación de vehículos en la sustitución de tags.
- Se mejoró el manejo de datos provenientes de REPUVE.
2026-03-27 14:40:34 -06:00
Juan Felipe Zapata Moreno
c0b263eb87 feat: agrega campo folio_anterior en VehicleTagLog y actualiza lógica de consulta en constanciasSustituidas 2026-03-25 10:11:57 -06:00
Juan Felipe Zapata Moreno
3a3c0a2ebf feat: actualiza validaciones en vehicleUpdate para placa y agrega validación de teléfono 2026-03-25 09:13:38 -06:00
Juan Felipe Zapata Moreno
828dd8fa21 feat: agrega servicio de consulta a REPUVE y Padrón Estatal en los controladores de Excel y Record 2026-03-20 15:58:22 -06:00
767e7abf5b hotfix: validación sustitucion 2026-03-20 10:56:30 -06:00
Juan Felipe Zapata Moreno
633198e5ae feat: mejora la consulta y presentación de datos en la generación de constancias de sustitución de TAGs 2026-03-19 16:59:57 -06:00
Juan Felipe Zapata Moreno
f5c4fce98a feat: agrega manejo de folio y identificador de constancia de inscripción en el proceso de inscripción de vehículos 2026-03-14 11:02:17 -06:00
Juan Felipe Zapata Moreno
c3cad386aa fix: corrige el método de autorización para verificar permisos de APK 2026-03-12 18:24:03 -06:00
Juan Felipe Zapata Moreno
153c296ea1 fix: corrige el puerto de MySQL en el archivo docker-compose 2026-03-12 17:57:06 -06:00
Juan Felipe Zapata Moreno
2410f13320 feat: actualiza la migración para asignar permisos de APK a roles existentes 2026-03-12 17:14:12 -06:00
Juan Felipe Zapata Moreno
2237572d1f feat: agrega APP_KEY a las variables de entorno en los archivos docker-compose 2026-03-12 17:01:34 -06:00
Juan Felipe Zapata Moreno
7510123626 feat: mejora la construcción de cadenas para las solicitudes de verificación y consulta de vehículos 2026-03-12 15:33:25 -06:00
Juan Felipe Zapata Moreno
c0d0e8dd86 feat: actualiza el sistema de logging para usar un canal específico para REPUVE Nacional 2026-03-12 14:56:09 -06:00
Juan Felipe Zapata Moreno
1566c891a5 feat: elimina referencia a la descarga de APK en el seeder de roles 2026-03-12 13:14:15 -06:00
Juan Felipe Zapata Moreno
15d42dbcec feat: elimina migración de reseed para la tabla de permisos 2026-03-12 13:04:13 -06:00
Juan Felipe Zapata Moreno
3ca44ea26b feat: agrega logging detallado para consultas al padrón estatal 2026-03-12 12:56:16 -06:00
Juan Felipe Zapata Moreno
53f451c54c feat: agrega migraciones para permisos de gestión de APK 2026-03-12 12:23:17 -06:00
Juan Felipe Zapata Moreno
b8f210478e feat: agrega permisos para cargar APK 2026-03-12 11:45:46 -06:00
Juan Felipe Zapata Moreno
f7941af3cf feat: agrega permisos para la creación y gestión de APK en el controlador y la siembra de roles 2026-03-11 18:42:16 -06:00
Juan Felipe Zapata Moreno
19af2f4bef feat: implement APK versioning and management with upload, update, and download functionalities 2026-03-11 16:05:15 -06:00
Juan Felipe Zapata Moreno
5ec3e6d52a feat: reorganiza rutas de la aplicación móvil y elimina duplicados 2026-03-11 12:25:57 -06:00
Juan Felipe Zapata Moreno
2c5307fa5b feat: agrega funcionalidad para subir y descargar APK de la aplicación móvil 2026-03-11 12:22:39 -06:00
Juan Felipe Zapata Moreno
fc897f5130 feat: agrega función para Repuve nacional con logging detallado 2026-03-09 16:40:12 -06:00
Juan Felipe Zapata Moreno
28d008bf54 fix: corrige nombres de recursos en la siembra de roles y permisos 2026-03-05 12:18:20 -06:00
Juan Felipe Zapata Moreno
886d9cf3a2 fix: corrige puertos en la configuración de Docker para nginx y MySQL 2026-03-05 10:33:01 -06:00
Juan Felipe Zapata Moreno
4784bbdb9e feat: agrega soporte para generación de documentos Word y PDF, actualiza dependencias y mejora la configuración de Docker 2026-03-04 13:00:13 -06:00
Juan Felipe Zapata Moreno
3e7d381f15 feat: actualiza configuración de Docker, mejora la gestión de permisos y renombra campos en las vistas de constancias 2026-03-03 18:00:11 -06:00
e701d5348a Actualización de docker a docker-dev y docker-prod (#2)
Co-authored-by: Juan Felipe Zapata Moreno <zapata_pipe@hotmail.com>
Reviewed-on: #2
2026-03-03 22:40:41 +00:00
3bc216d28e FIX:Cambio de permiso module a modules 2026-03-03 11:41:37 -06:00
09d80e6726 FIX:Auth en edicion y creación de modulo 2026-03-03 11:11:33 -06:00
Juan Felipe Zapata Moreno
4c417be38c feat: actualiza controladores y solicitudes para unificar permisos y agrega método de desencriptación en SettingsController 2026-02-26 14:30:18 -06:00
Juan Felipe Zapata Moreno
ad6b19e9dd feat: actualiza controladores y solicitudes para mejorar la gestión de permisos y validaciones 2026-02-26 12:43:09 -06:00
Juan Felipe Zapata Moreno
0faabb3026 feat: actualiza DatabaseSeeder y RepuveSeeder para usar contraseñas seguras 2026-02-25 10:26:06 -06:00
Juan Felipe Zapata Moreno
5bad287ef4 feat: agrega middleware para control de permisos en PermissionTypeController y actualiza RoleController para incluir permisos de visualización 2026-02-24 10:14:18 -06:00
Juan Felipe Zapata Moreno
31746867b8 feat: Agrega validaciones de autorización y nuevas clases Request
- Se agregó autorización basada en permisos en múltiples Requests.
- Nuevos Requests para motivos de cancelación y tags con validación y autorización.
- Se añadieron métodos de roles al modelo User (isDeveloper, isAdmin, isPrimary).
- Se actualizó el acceso a Telescope usando validación por roles.
- Mejora en el manejo de excepciones de autorización.
- Actualización de RoleSeeder con nuevas convenciones de permisos.
- Actualización de dependencias (composer.lock).
2026-02-23 13:05:53 -06:00
69371a0088 Fix: restart change to unless-stoped 2026-02-21 10:04:22 -06:00
e846f00767 Fix: remove phpmyadmin container 2026-02-21 09:58:09 -06:00
0f9aefa131 Fix: nginx 2026-02-20 17:17:35 -06:00
Juan Felipe Zapata Moreno
feb240698f fix: ajustar condiciones en el controlador de roles y deshabilitar el campo de foto de perfil en el modelo de usuario 2026-02-20 13:34:58 -06:00
aeebda6faa fix: manejar excepciones específicas para el padrón estatal y mejorar la validación de respuestas 2026-02-17 23:12:35 -06:00
Juan Felipe Zapata Moreno
f04dbccedb fix: agregar validación única para el nombre del módulo y mensajes de error en las solicitudes de módulo 2026-02-17 14:41:34 -06:00
Juan Felipe Zapata Moreno
de9d801d50 fix: mejorar manejo de excepciones y validaciones en controladores de dispositivos, modulos y paquetes 2026-02-17 14:35:34 -06:00
Juan Felipe Zapata Moreno
43269ca04a fix: ajustar el filtrado de registros por módulo del usuario 2026-02-16 12:48:29 -06:00
2093ff7538 feat: inscripción de vehículo con datos de repuve nacional y estatal 2026-02-14 12:35:01 -06:00
Juan Felipe Zapata Moreno
557fe6858c add: implementar seeder para usuarios y módulos en RepuveSeeder 2026-02-13 13:49:55 -06:00
Juan Felipe Zapata Moreno
79c1043f7a fix: prevenir eliminación del rol 'admin' y actualizar permisos de búsqueda en inscripciones 2026-02-13 13:13:18 -06:00
Juan Felipe Zapata Moreno
2717176373 add: migración para roles y permisos 2026-02-13 10:20:20 -06:00
Juan Felipe Zapata Moreno
04c9fe2d5a fix: actualizar tipos de acción en registros de vehículos y migración para 'sustitucion_primera_vez' 2026-02-11 15:36:46 -06:00
Juan Felipe Zapata Moreno
23ba2a03f9 fix: actualizar lógica de cancelación y agregar campos de placas y NIV en la vista de constancia 2026-02-07 13:58:54 -06:00
Juan Felipe Zapata Moreno
d65711106f fix: actualizar etiquetas en la plantilla de constancia de sustitución 2026-01-26 10:51:08 -06:00
Juan Felipe Zapata Moreno
b1f05e6267 fix: pdf impresión de constancia 2026-01-26 10:23:53 -06:00
Juan Felipe Zapata Moreno
64196c9d5b fix: cambios a excel, pdf de constancias dañadas y canceladas, logos a excel 2026-01-26 09:21:10 -06:00
Juan Felipe Zapata Moreno
392b155367 fix: actualizar datos del expediente 2026-01-22 14:17:46 -06:00
43c1400e7c fix: excluir actualización al generar excel vista consulta para admin 2026-01-21 19:35:07 -06:00
c725072291 fix: excel de registros con historial de tags 2026-01-21 19:28:12 -06:00
b602687233 fix: error checar si tiene reporte de robo siempre true no dejaba continuar con el proceso 2026-01-21 19:07:42 -06:00
Juan Felipe Zapata Moreno
ebae87f97c fix: excel actualizaciones filtrar por modulo 2026-01-21 16:29:27 -06:00
Juan Felipe Zapata Moreno
e4419f1a50 fix: module_id al cancelar tag dañado 2026-01-21 14:08:32 -06:00
Juan Felipe Zapata Moreno
69727724d3 fix: se agregó actualizaciones al excel general 2026-01-21 12:11:10 -06:00
Juan Felipe Zapata Moreno
6eeba6f9fe feat: agregar generación de PDF para tags dañados y actualizar rutas de Excel 2026-01-21 12:03:45 -06:00
Juan Felipe Zapata Moreno
6106afbecf refactor: permisos y roles 2026-01-20 12:44:08 -06:00
Juan Felipe Zapata Moreno
6d27b9a818 hotfix permisos 2026-01-20 12:33:20 -06:00
Juan Felipe Zapata Moreno
63a2a3a338 fix 2026-01-20 12:29:15 -06:00
Juan Felipe Zapata Moreno
977d5a0420 fix: permisos de encargado 2026-01-20 12:23:52 -06:00
Juan Felipe Zapata Moreno
ea4e3fc607 fix: actualizar tipos de acción a 'sustitución' en InscriptionController 2026-01-20 10:41:23 -06:00
Juan Felipe Zapata Moreno
2411ff1eab fix: excel searchRecord 2026-01-20 10:07:03 -06:00
Juan Felipe Zapata Moreno
1d7afe1b9a fix: email reemplazado por username en controladores 2026-01-19 16:30:21 -06:00
Juan Felipe Zapata Moreno
927c46aa2e fix: login con username 2026-01-19 16:12:46 -06:00
ee9b265582 fix: razon de cancelación 2026-01-17 11:18:47 -06:00
50e0cff497 fix: pacakges/boxes razones de cancelación 2026-01-17 01:13:28 -06:00
Juan Felipe Zapata Moreno
974efc1c9c fix: actualizar dispositivo 2026-01-16 09:53:29 -06:00
Juan Felipe Zapata Moreno
22f7e0226e fix: creación de dispositivo 2026-01-16 09:29:03 -06:00
Juan Felipe Zapata Moreno
0a7b46c0bc fix: ruta generar excel del tag history 2026-01-15 12:58:09 -06:00
Juan Felipe Zapata Moreno
ce7e1f998e add: generar excel del tag history 2026-01-15 12:55:58 -06:00
Juan Felipe Zapata Moreno
c0225a69e2 fix: dispositivos moviles usuario nullable 2026-01-15 09:27:25 -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
189 changed files with 19717 additions and 4201 deletions

View File

@ -34,9 +34,9 @@ DB_PORT=3306
DB_DATABASE=holos-backend DB_DATABASE=holos-backend
DB_USERNAME=notsoweb DB_USERNAME=notsoweb
DB_PASSWORD= DB_PASSWORD=
DB_ROOT_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 +75,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=

5
.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
@ -24,3 +25,7 @@ yarn-error.log
/.nova /.nova
/.vscode /.vscode
/.zed /.zed
CLAUDE.md
/Docker/QA/.env.qa
/Docker/Dev/.env.dev
/Docker/Prod/.env.prod

View File

@ -0,0 +1,97 @@
APP_NAME="Holos"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=America/Mexico_City
APP_URL=http://backend.holos.test
APP_FRONTEND_URL=http://frontend.holos.test
APP_PAGINATION=25
APP_LOCALE=es
APP_FALLBACK_LOCALE=es
APP_FAKER_LOCALE=es_MX
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
CORS_ALLOWED_ORIGINS=*
PULSE_ENABLED=false
TELESCOPE_ENABLED=false
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=holos-backend
DB_USERNAME=notsoweb
DB_PASSWORD=
DB_ROOT_PASSWORD=
PMA_PORT=8081 # Puerto para phpMyAdmin
NGINX_PORT=8080 # Puerto para Nginx
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mail.smtp2go.com
MAIL_PORT=465
MAIL_DOMAIN=notsoweb.com
MAIL_USERNAME=no-reply@notsoweb.com
MAIL_PASSWORD=
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS="no-reply@notsoweb.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
# REPUVE FEDERAL
REPUVE_FED_BASE_URL=
REPUVE_FED_USERNAME=
REPUVE_FED_PASSWORD=
# REPUVE ESTATAL
REPUVE_EST_URL=
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_APP_NAME="${APP_NAME}"
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

25
Docker/Dev/config.alloy Normal file
View File

@ -0,0 +1,25 @@
logging {
level = "info"
format = "logfmt"
}
// Descubrir archivos de log
local.file_match "laravel_logs" {
path_targets = [
{ __path__ = "/var/log/repuve/padron-estatal.log", job = "padron_estatal", env = "dev" },
{ __path__ = "/var/log/repuve/repuve-nacional.log", job = "repuve_nacional", env = "dev" },
]
}
// Leer los archivos
loki.source.file "laravel_reader" {
targets = local.file_match.laravel_logs.targets
forward_to = [loki.write.local.receiver]
}
// Enviar a Loki
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}

View File

@ -0,0 +1,128 @@
name: repuve-backend-dev
services:
repuve-backend:
container_name: backend-dev
build:
context: ../../
dockerfile: Docker/Dev/dockerfile
working_dir: /var/www/repuve-backend-v1
environment:
- APP_ENV=development
- APP_DEBUG=true
- APP_KEY=${APP_KEY}
- DB_HOST=mysql
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}
- DB_PORT=${DB_PORT}
volumes:
- ../../storage:/var/www/repuve-backend-v1/storage
networks:
- repuve-dev-network
mem_limit: 256M
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
nginx:
container_name: repuve-nginx-dev
image: nginx:alpine
ports:
- "${NGINX_PORT}:80"
volumes:
- ../../public:/var/www/repuve-backend-v1/public
- ../../storage:/var/www/repuve-backend-v1/storage
- ../../Docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- /var/log/nginx:/var/log/nginx
logging:
driver: "local"
options:
max-size: "50m"
max-file: "10"
networks:
- repuve-dev-network
mem_limit: 128M
restart: unless-stopped
depends_on:
- repuve-backend
mysql:
container_name: repuve-mysql-dev
image: mysql:8.0
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
ports:
- "${DB_PORT_FORWARD}:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- repuve-dev-network
mem_limit: 256M
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 15s
retries: 10
alloy:
image: grafana/alloy:latest
command:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
ports:
- "12345:12345"
volumes:
- ./config.alloy:/etc/alloy/config.alloy
- ../../storage/logs:/var/log/repuve:ro
networks:
- repuve-dev-network
restart: unless-stopped
depends_on:
- repuve-backend
- loki
loki:
image: grafana/loki:latest
user: "0"
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- repuve-dev-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "8702:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- repuve-dev-network
restart: unless-stopped
depends_on:
- loki
volumes:
mysql_data:
driver: local
loki_data:
driver: local
grafana_data:
driver: local
networks:
repuve-dev-network:
driver: bridge

47
Docker/Dev/dockerfile Normal file
View File

@ -0,0 +1,47 @@
FROM php:8.3-fpm-alpine
WORKDIR /var/www/repuve-backend-v1
RUN apk add --no-cache \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
nano \
openssl \
bash \
mysql-client \
libreoffice \
ttf-dejavu \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --optimize-autoloader --no-interaction --no-scripts
COPY . .
COPY entrypoint-dev.sh /usr/local/bin/entrypoint-dev.sh
RUN chmod +x /usr/local/bin/entrypoint-dev.sh
COPY Docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
RUN chown -R www-data:www-data /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
RUN chmod -R 775 /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/entrypoint-dev.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -0,0 +1,39 @@
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-active
cache_location: /loki/tsdb-cache
filesystem:
directory: /loki/chunks
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
compactor:
working_directory: /loki/compactor

144
Docker/Dev/nginx.conf Normal file
View File

@ -0,0 +1,144 @@
user nginx;
worker_processes auto;
# Log de errores con máximo nivel de detalle
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
log_format forensic_main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" "$http_x_real_ip" '
'rt=$request_time ' # Tiempo total de la petición
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
'cs=$upstream_cache_status ' # Estado de caché
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
'conn=$connection ' # ID de conexión
'conn_reqs=$connection_requests ' # Peticiones por conexión
'pipe=$pipe ' # Pipelining (y/n)
'host="$host" ' # Host solicitado
'server_name="$server_name" '
'scheme="$scheme" '
'request_method="$request_method" '
'request_uri="$request_uri" '
'server_port="$server_port" '
'http_version="$server_protocol" '
'bytes_sent=$bytes_sent ' # Total bytes enviados
'request_length=$request_length ' # Tamaño de la petición
'req_id="$request_id"'; # ID único por petición
# Formato adicional para headers sensibles / seguridad
log_format forensic_headers
'$remote_addr [$time_local] req_id="$request_id" '
'Authorization="$http_authorization" '
'Cookie="$http_cookie" '
'Content-Type="$content_type" '
'Content-Length="$content_length" '
'Accept="$http_accept" '
'Accept-Language="$http_accept_language" '
'Accept-Encoding="$http_accept_encoding" '
'Origin="$http_origin" '
'Sec-Fetch-Site="$http_sec_fetch_site" '
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
'X-Custom-Header="$http_x_custom_header"';
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
error_log /var/log/nginx/error.log debug;
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off; # No revelar versión en respuestas (buena práctica)
# Añadir request_id único a cada petición
add_header X-Request-ID $request_id always;
# ─── SERVER BLOCK LARAVEL ───────────────────────────────────────────────────
server {
listen 80;
server_name _;
root /var/www/repuve-backend-v1/public;
index index.php index.html;
# Logging con formatos forenses (definidos en nginx.conf principal)
error_log /var/log/nginx/error.log debug;
access_log /var/log/nginx/access.log forensic_main;
# Handle Laravel routes (Front Controller)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Handle PHP files
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass backend-dev:9000;
fastcgi_index index.php;
# Timeouts importantes para evitar errores 500
fastcgi_read_timeout 300;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
# Carga los parámetros por defecto
include fastcgi_params;
# Parámetros críticos para Laravel
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_PROXY "";
# Añadir Request ID al backend para tracking
fastcgi_param HTTP_X_REQUEST_ID $request_id;
}
client_max_body_size 150M;
# Handle storage files (Laravel storage link)
location /storage/ {
alias /var/www/repuve-backend-v1/storage/app/public/;
}
location /profile {
alias /var/www/repuve-backend-v1/storage/app/profile;
try_files $uri =404;
}
location /images {
alias /var/www/repuve-backend-v1/storage/app/images;
try_files $uri =404;
}
# Denegar acceso a archivos ocultos como .htaccess
location ~ /\.ht {
deny all;
}
}
}

26
Docker/Prod/config.alloy Normal file
View File

@ -0,0 +1,26 @@
logging {
level = "warn"
format = "logfmt"
}
// Descubrir archivos de log de Laravel
local.file_match "laravel_logs" {
path_targets = [
{ __path__ = "/var/log/repuve/padron-estatal.log", job = "padron_estatal", env = "prod" },
{ __path__ = "/var/log/repuve/repuve-nacional.log", job = "repuve_nacional", env = "prod" },
{ __path__ = "/var/log/mysql/general.log", job = "db_general", env = "prod"},
]
}
// Leer los archivos
loki.source.file "laravel_reader" {
targets = local.file_match.laravel_logs.targets
forward_to = [loki.write.local.receiver]
}
// Enviar a Loki
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}

21
Docker/Prod/custom-my.cnf Normal file
View File

@ -0,0 +1,21 @@
[mysqld]
# --- Log de Errores ---
log_error = /var/log/mysql/error.log
# --- Log General (Consultas y Debug) ---
general_log = 1
general_log_file = /var/log/mysql/general.log
# --- Registro de Transacciones (Binary Log) ---
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
expire_logs_days = 7
# --- Consultas Lentas (Slow Query Log) ---
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# --- Depuración de InnoDB ---
innodb_print_all_deadlocks = 1

View File

@ -0,0 +1,130 @@
name: repuve-backend-prod
services:
repuve-backend:
container_name: backend-prod
build:
context: ../../
dockerfile: Docker/Dev/dockerfile
working_dir: /var/www/repuve-backend-v1
environment:
- APP_ENV=development
- APP_DEBUG=true
- APP_KEY=${APP_KEY}
- DB_HOST=mysql
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}
- DB_PORT=${DB_PORT}
volumes:
- ../../storage:/var/www/repuve-backend-v1/storage
networks:
- repuve-prod-network
mem_limit: 512M
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
nginx:
container_name: repuve-nginx-prod
image: nginx:alpine
ports:
- "${NGINX_PORT}:80"
volumes:
- ../../public:/var/www/repuve-backend-v1/public
- ../../storage:/var/www/repuve-backend-v1/storage
- ../../Docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- /var/log/nginx:/var/log/nginx
logging:
driver: "local"
options:
max-size: "50m"
max-file: "10"
networks:
- repuve-prod-network
mem_limit: 128M
restart: unless-stopped
depends_on:
- repuve-backend
mysql:
container_name: repuve-mysql-prod
image: mysql:8.0
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
ports:
- "${DB_PORT_FORWARD}:3306"
volumes:
- mysql_data:/var/lib/mysql
# Montamos el archivo de configuración
- ./custom-my.cnf:/etc/mysql/conf.d/custom-my.cnf
# Montamos el directorio de logs para persistencia y acceso desde el host
- ./logs:/var/log/mysql
networks:
- repuve-prod-network
mem_limit: 512M
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 15s
retries: 10
alloy:
image: grafana/alloy:latest
command:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
ports:
- "12345:12345"
volumes:
- ./config.alloy:/etc/alloy/config.alloy
- ../../storage/logs:/var/log/repuve:ro
- ./logs:/var/log/mysql:ro
networks:
- repuve-prod-network
restart: unless-stopped
depends_on:
- repuve-backend
- loki
loki:
image: grafana/loki:latest
user: "0"
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- repuve-prod-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "8700:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- repuve-prod-network
restart: unless-stopped
depends_on:
- loki
volumes:
mysql_data:
driver: local
loki_data:
driver: local
grafana_data:
driver: local
networks:
repuve-prod-network:
driver: bridge

47
Docker/Prod/dockerfile Normal file
View File

@ -0,0 +1,47 @@
FROM php:8.3-fpm-alpine
WORKDIR /var/www/repuve-backend-v1
RUN apk add --no-cache \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
nano \
openssl \
bash \
mysql-client \
libreoffice \
ttf-dejavu \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --optimize-autoloader --no-interaction --no-scripts
COPY . .
COPY entrypoint-dev.sh /usr/local/bin/entrypoint-dev.sh
RUN chmod +x /usr/local/bin/entrypoint-dev.sh
COPY Docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
RUN chown -R www-data:www-data /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
RUN chmod -R 775 /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/entrypoint-dev.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
jsonData:
maxLines: 1000

View File

@ -0,0 +1,39 @@
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-active
cache_location: /loki/tsdb-cache
filesystem:
directory: /loki/chunks
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
compactor:
working_directory: /loki/compactor

144
Docker/Prod/nginx.conf Normal file
View File

@ -0,0 +1,144 @@
user nginx;
worker_processes auto;
# Log de errores con máximo nivel de detalle
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
log_format forensic_main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" "$http_x_real_ip" '
'rt=$request_time ' # Tiempo total de la petición
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
'cs=$upstream_cache_status ' # Estado de caché
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
'conn=$connection ' # ID de conexión
'conn_reqs=$connection_requests ' # Peticiones por conexión
'pipe=$pipe ' # Pipelining (y/n)
'host="$host" ' # Host solicitado
'server_name="$server_name" '
'scheme="$scheme" '
'request_method="$request_method" '
'request_uri="$request_uri" '
'server_port="$server_port" '
'http_version="$server_protocol" '
'bytes_sent=$bytes_sent ' # Total bytes enviados
'request_length=$request_length ' # Tamaño de la petición
'req_id="$request_id"'; # ID único por petición
# Formato adicional para headers sensibles / seguridad
log_format forensic_headers
'$remote_addr [$time_local] req_id="$request_id" '
'Authorization="$http_authorization" '
'Cookie="$http_cookie" '
'Content-Type="$content_type" '
'Content-Length="$content_length" '
'Accept="$http_accept" '
'Accept-Language="$http_accept_language" '
'Accept-Encoding="$http_accept_encoding" '
'Origin="$http_origin" '
'Sec-Fetch-Site="$http_sec_fetch_site" '
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
'X-Custom-Header="$http_x_custom_header"';
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
error_log /var/log/nginx/error.log debug;
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off; # No revelar versión en respuestas (buena práctica)
# Añadir request_id único a cada petición
add_header X-Request-ID $request_id always;
# ─── SERVER BLOCK LARAVEL ───────────────────────────────────────────────────
server {
listen 80;
server_name _;
root /var/www/repuve-backend-v1/public;
index index.php index.html;
# Logging con formatos forenses (definidos en nginx.conf principal)
error_log /var/log/nginx/error.log debug;
access_log /var/log/nginx/access.log forensic_main;
# Handle Laravel routes (Front Controller)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Handle PHP files
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass backend-dev:9000;
fastcgi_index index.php;
# Timeouts importantes para evitar errores 500
fastcgi_read_timeout 300;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
# Carga los parámetros por defecto
include fastcgi_params;
# Parámetros críticos para Laravel
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_PROXY "";
# Añadir Request ID al backend para tracking
fastcgi_param HTTP_X_REQUEST_ID $request_id;
}
client_max_body_size 150M;
# Handle storage files (Laravel storage link)
location /storage/ {
alias /var/www/repuve-backend-v1/storage/app/public/;
}
location /profile {
alias /var/www/repuve-backend-v1/storage/app/profile;
try_files $uri =404;
}
location /images {
alias /var/www/repuve-backend-v1/storage/app/images;
try_files $uri =404;
}
# Denegar acceso a archivos ocultos como .htaccess
location ~ /\.ht {
deny all;
}
}
}

97
Docker/QA/.env.qa.example Normal file
View File

@ -0,0 +1,97 @@
APP_NAME="Holos"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=America/Mexico_City
APP_URL=http://backend.holos.test
APP_FRONTEND_URL=http://frontend.holos.test
APP_PAGINATION=25
APP_LOCALE=es
APP_FALLBACK_LOCALE=es
APP_FAKER_LOCALE=es_MX
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
CORS_ALLOWED_ORIGINS=*
PULSE_ENABLED=false
TELESCOPE_ENABLED=false
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=holos-backend
DB_USERNAME=notsoweb
DB_PASSWORD=
DB_ROOT_PASSWORD=
PMA_PORT=8081 # Puerto para phpMyAdmin
NGINX_PORT=8080 # Puerto para Nginx
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mail.smtp2go.com
MAIL_PORT=465
MAIL_DOMAIN=notsoweb.com
MAIL_USERNAME=no-reply@notsoweb.com
MAIL_PASSWORD=
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS="no-reply@notsoweb.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
# REPUVE FEDERAL
REPUVE_FED_BASE_URL=
REPUVE_FED_USERNAME=
REPUVE_FED_PASSWORD=
# REPUVE ESTATAL
REPUVE_EST_URL=
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_APP_NAME="${APP_NAME}"
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

25
Docker/QA/config.alloy Normal file
View File

@ -0,0 +1,25 @@
logging {
level = "info"
format = "logfmt"
}
// Descubrir archivos de log de Laravel
local.file_match "laravel_logs" {
path_targets = [
{ __path__ = "/var/log/repuve/padron-estatal.log", job = "padron_estatal", env = "qa" },
{ __path__ = "/var/log/repuve/repuve-nacional.log", job = "repuve_nacional", env = "qa" },
]
}
// Leer los archivos
loki.source.file "laravel_reader" {
targets = local.file_match.laravel_logs.targets
forward_to = [loki.write.local.receiver]
}
// Enviar a Loki
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}

View File

@ -0,0 +1,129 @@
name: repuve-backend-qa
services:
repuve-backend:
container_name: backend-qa
build:
context: ../../
dockerfile: Docker/QA/dockerfile
working_dir: /var/www/repuve-backend-v1
environment:
- APP_ENV=qa
- APP_DEBUG=true
- APP_KEY=${APP_KEY}
- DB_HOST=${DB_HOST}
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}
- DB_PORT=${DB_PORT}
volumes:
- ../../storage:/var/www/repuve-backend-v1/storage
networks:
- repuve-qa-network
mem_limit: 256M
restart: unless-stopped
depends_on:
qa-mysql:
condition: service_healthy
nginx:
container_name: repuve-nginx-qa
image: nginx:alpine
ports:
- "${NGINX_PORT}:80"
volumes:
- ../../public:/var/www/repuve-backend-v1/public
- ../../storage:/var/www/repuve-backend-v1/storage
- ./nginx.conf:/etc/nginx/nginx.conf
- /var/log/nginx:/var/log/nginx
logging:
driver: "local"
options:
max-size: "50m"
max-file: "10"
networks:
- repuve-qa-network
mem_limit: 128M
restart: unless-stopped
depends_on:
- repuve-backend
qa-mysql:
container_name: repuve-mysql-qa
image: mysql:8.0
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
ports:
- "${DB_PORT_FORWARD}:3306"
volumes:
- qa_mysql_data:/var/lib/mysql
networks:
- repuve-qa-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
mem_limit: 256M
restart: unless-stopped
alloy:
image: grafana/alloy:latest
command:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
ports:
- "12346:12345"
volumes:
- ./config.alloy:/etc/alloy/config.alloy
- ../../storage/logs:/var/log/repuve:ro
networks:
- repuve-qa-network
restart: unless-stopped
depends_on:
- repuve-backend
- loki
loki:
image: grafana/loki:latest
user: "0"
ports:
- "3200:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml
- qa_loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- repuve-qa-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "8701:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
volumes:
- qa_grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- repuve-qa-network
restart: unless-stopped
depends_on:
- loki
networks:
repuve-qa-network:
driver: bridge
volumes:
qa_mysql_data:
qa_loki_data:
driver: local
qa_grafana_data:
driver: local

47
Docker/QA/dockerfile Normal file
View File

@ -0,0 +1,47 @@
FROM php:8.3-fpm-alpine
WORKDIR /var/www/repuve-backend-v1
RUN apk add --no-cache \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
nano \
openssl \
bash \
mysql-client \
libreoffice \
ttf-dejavu \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --optimize-autoloader --no-interaction --no-scripts
COPY . .
COPY entrypoint-dev.sh /usr/local/bin/entrypoint-dev.sh
RUN chmod +x /usr/local/bin/entrypoint-dev.sh
COPY Docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
RUN chown -R www-data:www-data /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
RUN chmod -R 775 /var/www/repuve-backend-v1/storage /var/www/repuve-backend-v1/bootstrap/cache
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/entrypoint-dev.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
jsonData:
maxLines: 1000

39
Docker/QA/loki-config.yml Normal file
View File

@ -0,0 +1,39 @@
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-active
cache_location: /loki/tsdb-cache
filesystem:
directory: /loki/chunks
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
compactor:
working_directory: /loki/compactor

144
Docker/QA/nginx.conf Normal file
View File

@ -0,0 +1,144 @@
user nginx;
worker_processes auto;
# Log de errores con máximo nivel de detalle
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
log_format forensic_main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" "$http_x_real_ip" '
'rt=$request_time ' # Tiempo total de la petición
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
'cs=$upstream_cache_status ' # Estado de caché
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
'conn=$connection ' # ID de conexión
'conn_reqs=$connection_requests ' # Peticiones por conexión
'pipe=$pipe ' # Pipelining (y/n)
'host="$host" ' # Host solicitado
'server_name="$server_name" '
'scheme="$scheme" '
'request_method="$request_method" '
'request_uri="$request_uri" '
'server_port="$server_port" '
'http_version="$server_protocol" '
'bytes_sent=$bytes_sent ' # Total bytes enviados
'request_length=$request_length ' # Tamaño de la petición
'req_id="$request_id"'; # ID único por petición
# Formato adicional para headers sensibles / seguridad
log_format forensic_headers
'$remote_addr [$time_local] req_id="$request_id" '
'Authorization="$http_authorization" '
'Cookie="$http_cookie" '
'Content-Type="$content_type" '
'Content-Length="$content_length" '
'Accept="$http_accept" '
'Accept-Language="$http_accept_language" '
'Accept-Encoding="$http_accept_encoding" '
'Origin="$http_origin" '
'Sec-Fetch-Site="$http_sec_fetch_site" '
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
'X-Custom-Header="$http_x_custom_header"';
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
error_log /var/log/nginx/error.log debug;
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off; # No revelar versión en respuestas (buena práctica)
# Añadir request_id único a cada petición
add_header X-Request-ID $request_id always;
# ─── SERVER BLOCK LARAVEL ───────────────────────────────────────────────────
server {
listen 80;
server_name _;
root /var/www/repuve-backend-v1/public;
index index.php index.html;
# Logging con formatos forenses (definidos en nginx.conf principal)
error_log /var/log/nginx/error.log debug;
access_log /var/log/nginx/access.log forensic_main;
# Handle Laravel routes (Front Controller)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Handle PHP files
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass backend-qa:9000;
fastcgi_index index.php;
# Timeouts importantes para evitar errores 500
fastcgi_read_timeout 300;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
# Carga los parámetros por defecto
include fastcgi_params;
# Parámetros críticos para Laravel
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_PROXY "";
# Añadir Request ID al backend para tracking
fastcgi_param HTTP_X_REQUEST_ID $request_id;
}
client_max_body_size 150M;
# Handle storage files (Laravel storage link)
location /storage/ {
alias /var/www/repuve-backend-v1/storage/app/public/;
}
location /profile {
alias /var/www/repuve-backend-v1/storage/app/profile;
try_files $uri =404;
}
location /images {
alias /var/www/repuve-backend-v1/storage/app/images;
try_files $uri =404;
}
# Denegar acceso a archivos ocultos como .htaccess
location ~ /\.ht {
deny all;
}
}
}

69
Docker/nginx/default.conf Normal file
View File

@ -0,0 +1,69 @@
server {
listen 80;
server_name _;
root /var/www/repuve-backend-v1/public;
index index.php index.html;
# Logging con formatos forenses (definidos en nginx.conf principal)
error_log /var/log/nginx/error.log debug;
access_log /var/log/nginx/access.log forensic_main;
# Handle Laravel routes (Front Controller)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Handle PHP files
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass repuve-backend:9000;
fastcgi_index index.php;
# Timeouts importantes para evitar errores 500
fastcgi_read_timeout 300;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
# Carga los parámetros por defecto
include fastcgi_params;
# Parámetros críticos para Laravel
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_PROXY "";
# Añadir Request ID al backend para tracking
fastcgi_param HTTP_X_REQUEST_ID $request_id;
}
client_max_body_size 20M;
# Handle storage files (Laravel storage link)
location /storage {
alias /var/www/repuve-backend-v1/storage/app/public;
try_files $uri =404;
}
location /profile {
alias /var/www/repuve-backend-v1/storage/app/profile;
try_files $uri =404;
}
location /images {
alias /var/www/repuve-backend-v1/storage/app/images;
try_files $uri =404;
}
# Denegar acceso a archivos ocultos como .htaccess
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,76 @@
user nginx;
worker_processes auto;
# Log de errores con máximo nivel de detalle
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
log_format forensic_main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" "$http_x_real_ip" '
'rt=$request_time ' # Tiempo total de la petición
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
'cs=$upstream_cache_status ' # Estado de caché
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
'conn=$connection ' # ID de conexión
'conn_reqs=$connection_requests ' # Peticiones por conexión
'pipe=$pipe ' # Pipelining (y/n)
'host="$host" ' # Host solicitado
'server_name="$server_name" '
'scheme="$scheme" '
'request_method="$request_method" '
'request_uri="$request_uri" '
'server_port="$server_port" '
'http_version="$server_protocol" '
'bytes_sent=$bytes_sent ' # Total bytes enviados
'request_length=$request_length ' # Tamaño de la petición
'req_id="$request_id"'; # ID único por petición
# Formato adicional para headers sensibles / seguridad
log_format forensic_headers
'$remote_addr [$time_local] req_id="$request_id" '
'Authorization="$http_authorization" '
'Cookie="$http_cookie" '
'Content-Type="$content_type" '
'Content-Length="$content_length" '
'Accept="$http_accept" '
'Accept-Language="$http_accept_language" '
'Accept-Encoding="$http_accept_encoding" '
'Origin="$http_origin" '
'Sec-Fetch-Site="$http_sec_fetch_site" '
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
'X-Custom-Header="$http_x_custom_header"';
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
error_log /var/log/nginx/error.log debug;
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off; # No revelar versión en respuestas (buena práctica)
# Añadir request_id único a cada petición
add_header X-Request-ID $request_id always;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -1,12 +1,87 @@
server { user nginx;
worker_processes auto;
# Log de errores con máximo nivel de detalle
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
log_format forensic_main
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for" "$http_x_real_ip" '
'rt=$request_time ' # Tiempo total de la petición
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
'cs=$upstream_cache_status ' # Estado de caché
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
'conn=$connection ' # ID de conexión
'conn_reqs=$connection_requests ' # Peticiones por conexión
'pipe=$pipe ' # Pipelining (y/n)
'host="$host" ' # Host solicitado
'server_name="$server_name" '
'scheme="$scheme" '
'request_method="$request_method" '
'request_uri="$request_uri" '
'server_port="$server_port" '
'http_version="$server_protocol" '
'bytes_sent=$bytes_sent ' # Total bytes enviados
'request_length=$request_length ' # Tamaño de la petición
'req_id="$request_id"'; # ID único por petición
# Formato adicional para headers sensibles / seguridad
log_format forensic_headers
'$remote_addr [$time_local] req_id="$request_id" '
'Authorization="$http_authorization" '
'Cookie="$http_cookie" '
'Content-Type="$content_type" '
'Content-Length="$content_length" '
'Accept="$http_accept" '
'Accept-Language="$http_accept_language" '
'Accept-Encoding="$http_accept_encoding" '
'Origin="$http_origin" '
'Sec-Fetch-Site="$http_sec_fetch_site" '
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
'X-Custom-Header="$http_x_custom_header"';
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
error_log /var/log/nginx/error.log debug;
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off; # No revelar versión en respuestas (buena práctica)
# Añadir request_id único a cada petición
add_header X-Request-ID $request_id always;
# ─── SERVER BLOCK LARAVEL ───────────────────────────────────────────────────
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 con formatos forenses (definidos en nginx.conf principal)
error_log /var/log/nginx/error.log; error_log /var/log/nginx/error.log debug;
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log forensic_main;
# Handle Laravel routes (Front Controller) # Handle Laravel routes (Front Controller)
location / { location / {
@ -17,7 +92,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
@ -39,23 +114,25 @@ server {
fastcgi_param HTTP_HOST $http_host; fastcgi_param HTTP_HOST $http_host;
fastcgi_param HTTPS $https if_not_empty; fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_PROXY ""; fastcgi_param HTTP_PROXY "";
# Añadir Request ID al backend para tracking
fastcgi_param HTTP_X_REQUEST_ID $request_id;
} }
client_max_body_size 20M; client_max_body_size 150M;
# 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;
} }
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;
} }
@ -64,3 +141,4 @@ server {
deny all; deny all;
} }
} }
}

View File

@ -0,0 +1,30 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:queue-worker]
command=php /var/www/repuve-backend-v1/artisan queue:work --tries=3 --timeout=300 --sleep=3 --max-time=3600
autostart=true
autorestart=true
priority=2
user=www-data
numprocs=1
stopwaitsecs=60
stdout_logfile=/var/www/repuve-backend-v1/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
stderr_logfile=/var/www/repuve-backend-v1/storage/logs/worker.log
stderr_logfile_maxbytes=0

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,5 @@
<?php
namespace App\Exceptions;
class PadronEstatalException extends \RuntimeException {}

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

@ -1,4 +1,7 @@
<?php namespace App\Http\Controllers\Admin; <?php
namespace App\Http\Controllers\Admin;
/** /**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/ */
@ -10,9 +13,9 @@
/** /**
* Eventos del usuarios del sistema * Eventos del usuarios del sistema
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class ActivityController extends Controller class ActivityController extends Controller
@ -24,27 +27,23 @@ public function index(UserActivityRequest $request)
{ {
$filters = $request->all(); $filters = $request->all();
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at'); $model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at')
->when(isset($filters['user']) && !empty($filters['user']), function ($query) use ($filters) {
if(isset($filters['user']) && !empty($filters['user'])){ $query->where('user_id', $filters['user']);
$model->where('user_id', $filters['user']); })
} ->when(isset($filters['search']) && !empty($filters['search']), function ($query) use ($filters) {
$query->where('event', 'like', '%' . $filters['search'] . '%');
if(isset($filters['search']) && !empty($filters['search'])){ })
$model->where('event', 'like', '%'.$filters['search'].'%'); ->when(isset($filters['start_date']) && !empty($filters['start_date']), function ($query) use ($filters) {
} $query->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
})
if(isset($filters['start_date']) && !empty($filters['start_date'])){ ->when(isset($filters['end_date']) && !empty($filters['end_date']), function ($query) use ($filters) {
$model->where('created_at', '>=', "{$filters['start_date']} 00:00:00"); $query->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
} });
if(isset($filters['end_date']) && !empty($filters['end_date'])){
$model->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
}
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'models' => 'models' =>
$model->orderBy('created_at', 'desc') $model->orderBy('created_at', 'desc')
->paginate(config('app.pagination')) ->paginate(config('app.pagination'))
]); ]);
} }

View File

@ -5,17 +5,26 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\PermissionType; use App\Models\PermissionType;
use Illuminate\Routing\Controllers\HasMiddleware;
use Notsoweb\ApiResponse\Enums\ApiResponse; use Notsoweb\ApiResponse\Enums\ApiResponse;
/** /**
* Tipos de permisos * Tipos de permisos
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class PermissionTypeController extends Controller class PermissionTypeController extends Controller implements HasMiddleware
{ {
public static function middleware(): array
{
return [
self::can('roles.index', ['all', 'allWithPermissions']),
];
}
/** /**
* Listar todo * Listar todo
*/ */
@ -31,8 +40,13 @@ public function all()
*/ */
public function allWithPermissions() public function allWithPermissions()
{ {
$hidden = ['Actividad', 'Cargar APK'];
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'models' => PermissionType::with('permissions')->orderBy('name')->get() 'models' => PermissionType::with('permissions')
->whereNotIn('name', $hidden)
->orderBy('name')
->get()
]); ]);
} }
} }

View File

@ -7,6 +7,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Roles\RoleStoreRequest; use App\Http\Requests\Roles\RoleStoreRequest;
use App\Http\Requests\Roles\RoleUpdateRequest; use App\Http\Requests\Roles\RoleUpdateRequest;
use Illuminate\Routing\Controllers\HasMiddleware;
use App\Models\Role; use App\Models\Role;
use App\Supports\QuerySupport; use App\Supports\QuerySupport;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -14,19 +15,31 @@
/** /**
* Roles del sistema * Roles del sistema
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class RoleController extends Controller class RoleController extends Controller implements HasMiddleware
{ {
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('roles.index', ['index', 'show']),
self::can('roles.destroy', ['destroy']),
self::can('roles.permissions', ['permissions', 'updatePermissions']),
];
}
/** /**
* Listar * Listar
*/ */
public function index() public function index()
{ {
$model = Role::orderBy('description'); $model = Role::where('id', '!=','1')->orderBy('description');
QuerySupport::queryByKey($model, request(), 'name'); QuerySupport::queryByKey($model, request(), 'name');
@ -41,9 +54,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,
]);
} }
/** /**
@ -71,6 +88,11 @@ public function update(RoleUpdateRequest $request, Role $role)
*/ */
public function destroy(Role $role) public function destroy(Role $role)
{ {
if (in_array($role->id, [1, 2])) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se puede eliminar este rol'
]);
}
$role->delete(); $role->delete();
return ApiResponse::OK->response(); return ApiResponse::OK->response();
@ -81,8 +103,12 @@ public function destroy(Role $role)
*/ */
public function permissions(Role $role) public function permissions(Role $role)
{ {
$permissions = $role->id === 2
? $role->permissions->filter(fn($p) => !str_starts_with($p->name, 'activities.'))
: $role->permissions;
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'permissions' => $role->permissions 'permissions' => $permissions->values()
]); ]);
} }

View File

@ -12,29 +12,52 @@
use App\Supports\QuerySupport; use App\Supports\QuerySupport;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Notsoweb\ApiResponse\Enums\ApiResponse; use Notsoweb\ApiResponse\Enums\ApiResponse;
use Illuminate\Routing\Controllers\HasMiddleware;
/** /**
* Controlador de usuarios * Controlador de usuarios
* *
* Permite la administración de los usuarios en general. * Permite la administración de los usuarios en general.
* *
* @author Moisés Cortés C <moises.cortes@notsoweb.com> * @author Moisés Cortés C <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class UserController extends Controller class UserController extends Controller implements HasMiddleware
{ {
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('users.index', ['index']),
self::can('users.destroy', ['destroy']),
];
}
/** /**
* Listar * Listar
*/ */
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', 'username']);
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'models' => $users->paginate(config('app.pagination')) 'users' => $users->select([
'id',
'name',
'paternal',
'maternal',
'username',
'module_id',
'deleted_at'
])->paginate(config('app.pagination'))
]); ]);
} }
@ -49,7 +72,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 +95,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']),
]);
} }
/** /**
@ -152,7 +185,7 @@ public function activity(UserActivityRequest $request, User $user)
} }
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'models' => 'models' =>
$model->orderBy('created_at', 'desc') $model->orderBy('created_at', 'desc')
->paginate(config('app.pagination')) ->paginate(config('app.pagination'))
]); ]);

View File

@ -1,16 +1,22 @@
<?php namespace App\Http\Controllers; <?php namespace App\Http\Controllers;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved use Illuminate\Routing\Controllers\Middleware;
*/
/** /**
* Controlador base * Controlador base
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
abstract class Controller abstract class Controller
{ {
//
/**
* Evaluar permisos de un usuario
*/
public static function can(string $permission, array $methods): Middleware
{
return new Middleware("permission:{$permission}", only: $methods);
}
} }

View File

@ -23,11 +23,11 @@ class AuthController extends Controller
*/ */
public function login(LoginRequest $request) public function login(LoginRequest $request)
{ {
$user = User::where('email', $request->get('email'))->first(); $user = User::where('username', $request->get('username'))->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' => ['Usuario no valido']
]); ]);
} }

View File

@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers\Repuve;
use App\Http\Controllers\Controller;
use App\Http\Requests\Repuve\ApkStorageRequest;
use App\Http\Requests\Repuve\ApkUpdateRequest;
use App\Models\ApkLog;
use App\Models\ApkVersion;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Support\Facades\Storage;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class AppController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('apk.index', ['index']),
self::can('apk.create', ['store']),
self::can('apk.edit', ['update']),
self::can('apk.destroy', ['destroy']),
self::can('apk.download', ['download']),
];
}
/**
* Listar versiones: actual, 2 anteriores y las no disponibles
*/
public function index()
{
$latestId = ApkVersion::latest()->value('id');
$versions = ApkVersion::with('uploader:id,name')
->withCount('logs')
->latest()
->get()
->map(function ($version) use ($latestId) {
$version->is_current = $version->id === $latestId;
$version->available = $version->id === $latestId;
return $version;
});
return ApiResponse::OK->response([
'current' => $versions->where('is_current', true)->first(),
'recent' => $versions->where('is_current', false)->take(2)->values(),
'unavailable' => $versions->where('is_current', false)->skip(2)->values(),
]);
}
/**
* Subir nueva versión del APK
*/
public function store(ApkStorageRequest $request)
{
$file = $request->file('apk');
$fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk';
// Eliminar APK anterior del storage
foreach (Storage::disk('public')->files('apk') as $existing) {
Storage::disk('public')->delete($existing);
}
$path = $file->storeAs('apk', $fileName, 'public');
ApkVersion::create([
'file_name' => $fileName,
'path' => $path,
'changelog' => $request->input('changelog'),
'uploaded_by' => auth()->id(),
]);
return ApiResponse::OK->response([
'message' => 'APK subido correctamente',
'file' => $fileName,
]);
}
/**
* Actualizar nombre y/o changelog de una versión
*/
public function update(ApkUpdateRequest $request, ApkVersion $app)
{
$data = $request->only(['changelog', 'file_name']);
if ($request->filled('file_name') && $request->input('file_name') !== $app->file_name) {
$newFileName = $request->input('file_name');
$newPath = 'apk/' . $newFileName;
if (Storage::disk('public')->exists($newPath)) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'Ya existe un archivo con ese nombre.',
]);
}
Storage::disk('public')->move($app->path, $newPath);
$data['file_name'] = $newFileName;
$data['path'] = $newPath;
}
$app->update($data);
return ApiResponse::OK->response([
'message' => 'Versión actualizada correctamente',
]);
}
/**
* Eliminar una versión
*/
public function destroy(ApkVersion $app)
{
Storage::disk('public')->delete($app->path);
$app->delete();
return ApiResponse::OK->response([
'message' => 'Versión eliminada correctamente',
]);
}
/**
* Descargar APK más reciente y registrar log
*/
public function download()
{
$version = ApkVersion::latest()->first();
if (!$version) {
abort(404, 'No hay APK disponible para descargar.');
}
ApkLog::create([
'apk_version_id' => $version->id,
'downloaded_by' => auth()->id(),
]);
return redirect(asset('storage/' . $version->path));
}
}

View File

@ -0,0 +1,395 @@
<?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;
use Illuminate\Routing\Controllers\HasMiddleware;
class CancellationController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('cancellations.cancel_constancia', ['cancelarConstancia']),
self::can('cancellations.cancel_tag_no_asignado', ['cancelarTagNoAsignado']),
];
}
/**
* 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: usar el enviado o el del usuario autenticado
$moduleId = $request->filled('module_id') ? $request->module_id : Auth::user()->module_id;
if ($moduleId) {
$tag->module_id = $moduleId;
$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->full_name))
? $lastCancellation->cancelledBy->full_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,108 @@
<?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\CatalogCancellationReasonStoreRequest;
use App\Http\Requests\Repuve\CatalogCancellationReasonUpdateRequest;
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(CatalogCancellationReasonStoreRequest $request)
{
$validated = $request->validated();
$reason = CatalogCancellationReason::create($validated);
return ApiResponse::CREATED->response([
'message' => 'Razón de cancelación creada exitosamente',
'data' => $reason,
]);
}
public function update(CatalogCancellationReasonUpdateRequest $request, $id)
{
$reason = CatalogCancellationReason::find($id);
if (!$reason) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Razón de cancelación no encontrada',
]);
}
$validated = $request->validated();
$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 App\Http\Controllers\Controller;
use App\Models\CatalogNameImg;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class CatalogNameImgController extends Controller
{
/**
* 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,297 @@
<?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;
use Illuminate\Routing\Controllers\HasMiddleware;
class DeviceController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
self::can('devices.index', ['index']),
self::can('devices.show', ['show']),
self::can('devices.destroy', ['destroy']),
self::can('devices.toggle_status', ['toggleStatus']),
];
}
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', []);
if (!empty($userIds)) {
// Si hay usuarios, crear un registro por cada usuario
foreach ($userIds as $userId) {
DeviceModule::create([
'device_id' => $device->id,
'module_id' => $request->module_id,
'user_id' => $userId,
'status' => true,
]);
}
} else {
// Si no hay usuarios, crear solo la relación device-module
DeviceModule::create([
'device_id' => $device->id,
'module_id' => $request->module_id,
'user_id' => null,
'status' => true,
]);
}
DB::commit();
$device->load('deviceModules.module');
return ApiResponse::CREATED->response([
'message' => 'Dispositivo creado exitosamente.',
'device' => [
'id' => $device->id,
'brand' => $device->brand,
'serie' => $device->serie,
'status' => $device->status,
'module' => $device->deviceModules->first()?->module,
],
]);
} 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 (ModelNotFoundException $e) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Dispositivo no encontrado.',
]);
} 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);
// Validar unicidad solo si los valores cambiaron
if ($request->filled('serie') && $request->serie !== $device->serie) {
if (Device::where('serie', $request->serie)->exists()) {
DB::rollBack();
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'serie' => ['El número de serie ya está en uso.'],
]);
}
}
if ($request->filled('mac_address') && $request->mac_address !== $device->mac_address) {
if (Device::where('mac_address', $request->mac_address)->exists()) {
DB::rollBack();
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'mac_address' => ['La dirección MAC ya está registrada.'],
]);
}
}
$device->update($request->only(['brand', 'serie', 'mac_address', 'status']));
DeviceModule::where('device_id', $device->id)->delete();
$userIds = $request->input('user_id', []);
if (!empty($userIds)) {
foreach ($userIds as $userId) {
DeviceModule::create([
'device_id' => $device->id,
'module_id' => $request->module_id,
'user_id' => $userId,
'status' => true,
]);
}
} else {
DeviceModule::create([
'device_id' => $device->id,
'module_id' => $request->module_id,
'user_id' => null,
'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,
'username' => $dm->user->username,
])
->unique('id')
->values(),
],
]);
} catch (ModelNotFoundException $e) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Dispositivo no encontrado.',
]);
} catch (\Exception $e) {
DB::rollBack();
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 (ModelNotFoundException $e) {
DB::rollBack();
return ApiResponse::NOT_FOUND->response([
'message' => 'Dispositivo no encontrado.',
]);
} 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,643 @@
<?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\Exceptions\PadronEstatalException;
use App\Services\RepuveService;
use App\Services\PadronEstatalService;
use App\Jobs\ProcessRepuveResponse;
use App\Supports\SoapParallelExecutor;
use Illuminate\Routing\Controllers\HasMiddleware;
class InscriptionController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('inscription.search', ['searchRecord']),
self::can('inscription.search.national', ['stolen']),
];
}
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,
]);
}
if (!$tag->module_id) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'El tag no tiene módulo asignado. Debe asignarse a un módulo antes de poder usarse.',
'folio' => $folio,
]);
}
// 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,
]);
}
// Obtener datos del servicio ESTATAL
$datosCompletosRaw = $this->padronEstatalService->getVehiculoByPlaca($placa);
// Extraer datos del servicio estatal
$vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosCompletosRaw);
$ownerData = $this->padronEstatalService->extraerDatosPropietario($datosCompletosRaw);
// Obtener NIV para consultar REPUVE Nacional
$niv = $vehicleDataEstatal['niv'];
if (empty($niv)) {
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'message' => 'El padrón estatal no retornó un NIV válido para la placa proporcionada.',
'placa' => $placa,
]);
}
// Consultar REPUVE Nacional y verificar robo en paralelo
$parallelRequests = [
'repuve' => $this->repuveService->prepareConsultarVehiculoRequest($niv, $placa),
'robo' => $this->repuveService->prepareVerificarRoboRequest($niv, $placa),
];
$parallelResults = SoapParallelExecutor::execute($parallelRequests);
// Parsear respuesta REPUVE Nacional
$repuveNacionalData = $this->repuveService->parseConsultarVehiculoResponse(
$parallelResults['repuve']['response'] ?: ''
);
// Verificar si hubo error en la consulta a REPUVE Nacional
if ($repuveNacionalData['has_error'] ?? false) {
DB::rollBack();
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al consultar REPUVE Nacional.',
'error' => $repuveNacionalData['error_message'] ?? 'Error desconocido',
]);
}
// Determinar si es inscripción primera vez o sustitución
// Si el folio de la constancia viene vacío, es primera vez; si no, es sustitución
$folioRepuve = $repuveNacionalData['folio_CI'] ?? null;
$actionType = empty($folioRepuve) ? 'sustitucion_primera_vez' : 'sustitucion';
// Parsear respuesta de robo
$roboResult = $this->repuveService->parseRoboResponse(
$parallelResults['robo']['response'] ?: '',
$niv ?: $placa ?: 'N/A'
);
// Solo bloquear si está marcado como robado
if ($roboResult['is_robado'] ?? false) {
DB::rollBack();
return ApiResponse::FORBIDDEN->response([
'message' => '¡El vehículo presenta reporte de robo! No se puede continuar con la inscripción.',
'niv' => $niv,
'placa' => $placa,
]);
}
// 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 con datos del Padrón Estatal (fuente primaria)
$vehicle = Vehicle::create(array_merge(
$vehicleDataEstatal,
['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' => $actionType,
'folio_anterior' => $actionType === 'sustitucion' ? $folioRepuve : null,
'cancellation_at' => $actionType === 'sustitucion' ? now() : null,
'performed_by' => Auth::id(),
]);
// Crear registro
$record = Record::create([
'folio' => $folio,
'vehicle_id' => $vehicle->id,
'user_id' => Auth::id(),
'module_id' => $tag->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,
];
}
}
// Agregar datos de la constancia de inscripción
$datosCompletosRaw['folio_CI'] = $folio;
$datosCompletosRaw['identificador_CI'] = $tagNumber;
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 (PadronEstatalException $e) {
DB::rollBack();
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al consultar el padrón estatal.',
'error' => $e->getMessage(),
]);
} 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:sustitucion_primera_vez,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::forUserModule(Auth::user())
->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,username,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',
'performedBy:id,name',
])->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) {
$vehicleLogs = $record->vehicle->vehicleTagLogs->sortBy('created_at');
$firstLog = $vehicleLogs->first(); // primer evento
// Historial: todos los logs excepto el primero, uno por evento
$tagsHistory = [];
foreach ($vehicleLogs->skip(1)->values() as $index => $log) {
$tag = $log->tag;
$tagsHistory[] = [
'order' => $index + 1,
'log_id' => $log->id,
'tag_id' => $log->tag_id,
'action_type' => $log->action_type,
'folio' => $tag?->folio,
'tag_number' => $tag?->tag_number,
'box_number' => $tag?->package?->box_number,
'status' => $tag?->status?->code ?? 'unknown',
'module' => $tag?->module ? ['id' => $tag->module->id, 'name' => $tag->module->name] : null,
'operator' => $log->performedBy ? ['id' => $log->performedBy->id, 'name' => $log->performedBy->name] : null,
'performed_at' => $log->created_at,
'cancelled_at' => $log->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 (siempre el primer evento)
'action_type' => $firstLog?->action_type,
'action_date' => $firstLog?->created_at ?? $record->created_at,
// HISTORIAL DE TRÁMITES
'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,
'username' => $record->user->username,
] : 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,54 @@
<?php namespace App\Http\Controllers\Repuve;
use App\Http\Controllers\Controller;
use App\Services\LogsService;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use Notsoweb\ApiResponse\Enums\ApiResponse;
/**
* Descripción
*/
class LogsController extends Controller implements HasMiddleware
{
public function __construct(private LogsService $logsService)
{
}
public static function middleware(): array
{
return [];
}
public function repuveLogs(Request $request)
{
$filters = $this->filters($request);
$logs = $this->logsService->readRepuve($filters);
return ApiResponse::OK->response([
'source' => 'repuve',
'filters' => $filters,
'logs' => $logs,
]);
}
public function padronEstatalLogs(Request $request)
{
$filters = $this->filters($request);
$logs = $this->logsService->readPadronEstatal($filters);
return ApiResponse::OK->response([
'source' => 'padron-estatal',
'filters' => $filters,
'logs' => $logs,
]);
}
private function filters(Request $request): array
{
return $request->validate([
'start_date' => ['nullable', 'date'],
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
]);
}
}

View File

@ -0,0 +1,269 @@
<?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 Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use Illuminate\Routing\Controllers\HasMiddleware;
class ModuleController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('modules.index', ['index']),
self::can('modules.show', ['show']),
self::can('modules.destroy', ['destroy']),
self::can('modules.toggle_status', ['toggleStatus']),
];
}
/**
* Listar módulos existentes
*/
public function index(Request $request)
{
try {
$modules = Module::with([
'responsible:id,name,username',
'municipality:id,code,name',
'users:id,name,paternal,maternal,username,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'),
'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,
'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 {
$module = Module::with([
'responsible:id,name,username',
'municipality:id,code,name',
'users:id,name,paternal,maternal,username,module_id',
'users.roles:id,name,description'
])->findOrFail($id);
return ApiResponse::OK->response([
'module' => $module,
]);
} catch (ModelNotFoundException $e) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Módulo no encontrado.',
]);
} 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,
'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,488 @@
<?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\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use App\Models\Package;
use App\Models\Tag;
use Illuminate\Routing\Controllers\HasMiddleware;
class PackageController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('packages.index', ['index']),
self::can('packages.show', ['show']),
self::can('packages.destroy', ['destroy']),
self::can('packages.box_tags', ['getBoxTags']),
];
}
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,username'
]);
} else {
$packages->with('user:id,name,username');
}
$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,
'username' => $package->user->username,
] : 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();
// Verificar folios duplicados globalmente (la restricción tags_folio_unique es global)
$conflicting = Tag::whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page])
->pluck('folio');
if ($conflicting->isNotEmpty()) {
DB::rollBack();
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'message' => 'Los folios ingresados ya están registrados.',
'errors' => [
'starting_page' => [
'Los folios ' . $conflicting->join(', ') . ' ya existen en el sistema.',
],
],
]);
}
$form = $request->validated();
$form['user_id'] = Auth::id();
$package = Package::create($form);
$statusAvailable = CatalogTagStatus::where('code', 'available')->firstOrFail();
$padLength = strlen($request->starting_page);
$numericStart = (int) $request->starting_page;
$numericEnd = (int) $request->ending_page;
for ($page = $numericStart; $page <= $numericEnd; $page++) {
Tag::create([
'folio' => str_pad($page, $padLength, '0', STR_PAD_LEFT),
'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(), 'tags_folio_unique')) {
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'message' => 'Uno o más folios del rango ingresado ya existen en el sistema.',
'errors' => [
'starting_page' => ['El rango de folios contiene duplicados. Verifica los valores e intenta de nuevo.'],
],
]);
}
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 (ModelNotFoundException $e) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Paquete no encontrado.',
]);
} 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::NOT_FOUND->response([
'message' => 'Error de base de datos al actualizar el paquete',
'error' => $e->getMessage(),
]);
} catch (\Exception $e) {
DB::rollBack();
return ApiResponse::NOT_FOUND->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', 'cancellationLogs.cancellationReason'])
->where('package_id', $package->id)
->orderBy('id', '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));
// 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,
'cancellation_reason' => in_array($tag->status?->code, ['cancelled', 'damaged']) && $tag->cancellationLogs->first() ? [
'reason' => $tag->cancellationLogs->first()->cancellationReason?->name,
'observations' => $tag->cancellationLogs->first()->cancellation_observations,
'cancelled_at' => $tag->cancellationLogs->first()->cancellation_at,
] : 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,
'username' => $package->user->username,
] : null,
],
'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,700 @@
<?php
namespace App\Http\Controllers\Repuve;
use App\Http\Controllers\Controller;
use App\Services\RepuveService;
use App\Services\PadronEstatalService;
use Barryvdh\DomPDF\Facade\Pdf;
use App\Models\Record;
use App\Models\Tag;
use App\Models\VehicleTagLog;
use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use Codedge\Fpdf\Fpdf\Fpdf;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use PhpOffice\PhpWord\TemplateProcessor;
use Symfony\Component\Process\Process;
class RecordController extends Controller implements HasMiddleware
{
private RepuveService $repuveService;
private PadronEstatalService $padronEstatalService;
public function __construct(RepuveService $repuveService, PadronEstatalService $padronEstatalService)
{
$this->repuveService = $repuveService;
$this->padronEstatalService = $padronEstatalService;
}
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('records.generate_pdf', ['generatePdf']),
self::can('records.generate_pdf_form', ['generatePdfForm']),
self::can('records.generate_pdf_constancia', ['generatePdfConstancia']),
self::can('records.generate_pdf_verification', ['generatePdfVerification']),
];
}
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);
$template = new TemplateProcessor(storage_path('app/templates/constancia.docx'));
$template->setValues([
'niv' => $record->vehicle->niv,
'placa' => mb_strtoupper($record->vehicle->placa, 'UTF-8'),
'marca' => mb_strtoupper($record->vehicle->marca, 'UTF-8'),
'linea' => mb_strtoupper($record->vehicle->linea, 'UTF-8'),
'modelo' => $record->vehicle->modelo,
'full_name' => mb_strtoupper($record->vehicle->owner->full_name, 'UTF-8'),
'callep' => mb_strtoupper($record->vehicle->owner->callep ?? '', 'UTF-8'),
'num_ext' => $record->vehicle->owner->num_ext ?? '',
'municipality' => mb_strtoupper($record->vehicle->owner->municipality->name ?? '', 'UTF-8'),
'tipo_servicio' => mb_strtoupper($record->vehicle->tipo_servicio, 'UTF-8'),
]);
$tempDocx = storage_path('app/temp/constancia_' . $id . '_' . uniqid() . '.docx');
$template->saveAs($tempDocx);
$profilePath = storage_path('app/temp/lo_profile_' . uniqid());
$process = new Process([
'libreoffice',
'--headless',
'--convert-to', 'pdf',
'--outdir', storage_path('app/temp'),
$tempDocx,
"-env:UserInstallation=file://{$profilePath}",
]);
$process->setTimeout(60);
$process->run();
@unlink($tempDocx);
if (!$process->isSuccessful()) {
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al convertir el documento a PDF.',
'error' => $process->getErrorOutput(),
]);
}
$pdfPath = storage_path('app/temp/' . pathinfo($tempDocx, PATHINFO_FILENAME) . '.pdf');
return response()->file($pdfPath, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="constancia-inscripcion-' . $id . '.pdf"',
])->deleteFileAfterSend(true);
}
/**
* 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',
])->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;
// Consultar REPUVE Nacional y Padrón Estatal para obtener datos oficiales del vehículo
$repuveData = $this->repuveService->consultarVehiculo($vehicle->niv, $vehicle->placa);
$padronRaw = $this->padronEstatalService->getVehiculoByNiv($vehicle->niv);
$padronData = $this->padronEstatalService->extraerDatosVehiculo($padronRaw);
$now = Carbon::now()->locale('es_MX');
$data = [
// Datos del vehículo desde REPUVE Nacional y Padrón Estatal
'marca' => strtoupper($repuveData['marca'] ?? ''),
'linea' => strtoupper($repuveData['linea'] ?? ''),
'modelo' => $repuveData['modelo'] ?? '',
'niv' => strtoupper($repuveData['niv'] ?? ''),
'numero_motor' => strtoupper($padronData['numero_motor'] ?? ''),
'placa' => strtoupper($padronData['placa'] ?? ''),
'folio' => $repuveData['folio_CI'] ?? '',
// 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 pdfDamagedTag(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->isDamaged()) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'Solo se puede generar PDF para tags dañados.',
'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_dañada_' . $tag->tag_number . '.pdf');
} catch (\Exception $e) {
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al generar el PDF.',
'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_cancelada', [
'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 {
$record = Record::with(['vehicle.tag'])->findOrFail($recordId);
$oldTagLog = VehicleTagLog::where('vehicle_id', $record->vehicle_id)
->where('action_type', 'sustitucion')
->whereNotNull('cancellation_at')
->latest()
->first();
if (!$oldTagLog) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró sustitución registrada para este vehículo.',
'record' => $recordId,
]);
}
$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);
$pdfFilename = 'constancia_sustituida_' . $oldTag->folio . '.pdf';
$pdf = Pdf::loadView('pdfs.tag_sustitution', [
'substitution' => $substitutionData,
'is_first_time' => false,
])
->setPaper('a4', 'portrait')
->setOptions([
'defaultFont' => 'sans-serif',
'isHtml5ParserEnabled' => true,
'isRemoteEnabled' => true,
]);
return $pdf->stream($pdfFilename);
} 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 ?? '',
'tag_number' => $tag->tag_number ?? '',
'placa' => '',
'niv' => '',
'motivo' => 'N/A',
'operador' => 'N/A',
'modulo' => 'No especificado',
'ubicacion' => 'No especificado',
];
// Cargar módulo del tag si existe
if ($tag->module_id) {
$tag->load('module');
if ($tag->module) {
$data['modulo'] = $tag->module->name ?? '';
$data['ubicacion'] = $tag->module->address ?? '';
}
}
// 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 ?? '';
} else {
// Si el tag no tiene vehicle_id, buscar en el último log
$lastLog = $tag->vehicleTagLogs()
->with('vehicle')
->whereNotNull('vehicle_id')
->latest('created_at')
->first();
if ($lastLog && $lastLog->vehicle) {
$data['id_chip'] = $lastLog->vehicle->id_chip ?? '';
$data['placa'] = $lastLog->vehicle->placa ?? '';
$data['niv'] = $lastLog->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->full_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', 'sustitucion'])
->with(['cancellationReason', 'cancelledBy', 'vehicle'])
->latest()
->first();
if ($vehicleTagLog) {
$data['motivo'] = $vehicleTagLog->cancellationReason->name ?? 'No especificado';
$data['operador'] = $vehicleTagLog->cancelledBy->full_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?->full_name ?? 'Sistema';
// módulo del usuario
if ($oldTagLog->cancelledBy) {
$this->loadUserModule($oldTagLog->cancelledBy, $data);
}
// datos del vehículo
if ($oldTagLog->vehicle) {
$data['placa'] = $oldTagLog->vehicle->placa ?? '';
$data['niv'] = $oldTagLog->vehicle->niv ?? '';
$currentVehicleTag = $oldTagLog->vehicle->tag;
$isFlowA = $currentVehicleTag && $currentVehicleTag->id === $tag->id;
if ($isFlowA) {
$data['folio'] = $oldTagLog->folio_anterior ?? $tag->folio ?? '';
$data['folio_sustituto'] = $tag->folio ?? '';
$data['id_chip'] = $tag->tag_number ?? '';
} else {
$data['folio_sustituto'] = $currentVehicleTag?->folio ?? '';
$data['id_chip'] = $currentVehicleTag?->tag_number ?? '';
}
}
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,591 @@
<?php
namespace App\Http\Controllers\Repuve;
use App\Http\Controllers\Controller;
use App\Http\Requests\Repuve\TagStoreRequest;
use App\Http\Requests\Repuve\TagUpdateRequest;
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;
use Illuminate\Routing\Controllers\HasMiddleware;
class TagsController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
self::can('tags.index', ['index']),
self::can('tags.create', ['tagStore']),
self::can('tags.assign_to_module', ['assignToModule']),
self::can('tags.show', ['show']),
self::can('tags.destroy', ['destroy']),
];
}
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(TagStoreRequest $request)
{
try {
$validated = $request->validated();
// 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'];
$padLength = strlen($validated['folio']);
// Verificar si el folio está fuera del rango actual del paquete
$packageUpdated = false;
$rangeChanges = [];
$missingTags = [];
$packageStartNumeric = (int) $package->starting_page;
$packageEndNumeric = (int) $package->ending_page;
// Caso 1: El folio es MENOR que el starting_page (crear tags intermedios)
if ($folioNumerico < $packageStartNumeric) {
$rangeChanges['starting_page'] = [
'old' => $package->starting_page,
'new' => $validated['folio'],
];
// Crear tags intermedios (desde el nuevo folio hasta el starting_page - 1)
for ($i = $folioNumerico + 1; $i < $packageStartNumeric; $i++) {
$folioIntermedio = str_pad($i, $padLength, '0', STR_PAD_LEFT);
// Verificar que el tag no exista
$existingTag = Tag::where('folio', $folioIntermedio)->where('package_id', $package->id)->first();
if (!$existingTag) {
Tag::create([
'folio' => $folioIntermedio,
'tag_number' => null,
'package_id' => $package->id,
'module_id' => null,
'status_id' => $statusAvailable->id,
'vehicle_id' => null,
]);
$missingTags[] = $folioIntermedio;
}
}
$package->starting_page = $validated['folio'];
$packageUpdated = true;
}
// Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios)
if ($folioNumerico > $packageEndNumeric) {
$rangeChanges['ending_page'] = [
'old' => $package->ending_page,
'new' => $validated['folio'],
];
// Crear tags intermedios (desde ending_page + 1 hasta el nuevo folio - 1)
for ($i = $packageEndNumeric + 1; $i < $folioNumerico; $i++) {
$folioIntermedio = str_pad($i, $padLength, '0', STR_PAD_LEFT);
// Verificar que el tag no exista
$existingTag = Tag::where('folio', $folioIntermedio)->where('package_id', $package->id)->first();
if (!$existingTag) {
Tag::create([
'folio' => $folioIntermedio,
'tag_number' => null,
'package_id' => $package->id,
'module_id' => null,
'status_id' => $statusAvailable->id,
'vehicle_id' => null,
]);
$missingTags[] = $folioIntermedio;
}
}
$package->ending_page = $validated['folio'];
$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 (\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(TagUpdateRequest $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'],
]);
}
$validated = $request->validated();
// 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

@ -16,9 +16,9 @@
/** /**
* Controlador de sesiones * Controlador de sesiones
* *
* @author Moisés Cortés C <moises.cortes@notsoweb.com> * @author Moisés Cortés C <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class LoginController extends Controller class LoginController extends Controller
@ -28,11 +28,11 @@ class LoginController extends Controller
*/ */
public function login(LoginRequest $request) public function login(LoginRequest $request)
{ {
$user = User::where('email', $request->get('email'))->first(); $user = User::where('username', $request->get('username'))->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']
]); ]);
} }
@ -56,27 +56,35 @@ public function logout()
/** /**
* Contraseña olvidada * Contraseña olvidada
* Nota: Sin email, el reset se maneja por token directo
*/ */
public function forgotPassword(ForgotRequest $request) public function forgotPassword(ForgotRequest $request)
{ {
$data = $request->validated(); $data = $request->validated();
$user = User::where('email', $data['email'])->first(); $user = User::where('username', $data['username'])->first();
if (!$user) {
return ApiResponse::NOT_FOUND->response([
'username' => ['Usuario no encontrado']
]);
}
try { try {
$token = $this->generateToken($user); $token = $this->generateToken($user);
$user->notify(new ForgotPasswordNotification($token)); // Sin email, retornar el token directamente (para uso administrativo)
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'is_sent' => true 'is_generated' => true,
'token' => $token,
'message' => 'Token generado. Válido por 15 minutos.',
]); ]);
} catch (\Throwable $th) { } catch (\Throwable $th) {
Log::channel('mail')->info("Email: {$data['email']}"); Log::channel('mail')->info("Username: {$data['username']}");
Log::channel('mail')->error($th->getMessage()); Log::channel('mail')->error($th->getMessage());
return ApiResponse::INTERNAL_ERROR->response([ return ApiResponse::INTERNAL_ERROR->response([
'is_sent' => false, 'is_generated' => false,
]); ]);
} }
} }
@ -87,7 +95,7 @@ public function forgotPassword(ForgotRequest $request)
public function resetPassword(ResetPasswordRequest $request) public function resetPassword(ResetPasswordRequest $request)
{ {
$data = $request->validated(); $data = $request->validated();
$model = ResetPassword::with('user')->where('token', $data['token'])->first(); $model = ResetPassword::with('user')->where('token', $data['token'])->first();
if(!$model){ if(!$model){
@ -142,4 +150,4 @@ private function deleteToken($token)
{ {
ResetPassword::where('token', $token)->delete(); ResetPassword::where('token', $token)->delete();
} }
} }

View File

@ -0,0 +1,120 @@
<?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;
use Illuminate\Routing\Controllers\HasMiddleware;
/**
* Descripción
*/
class SettingsController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
self::can('system.settings', ['show', 'update']),
];
}
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' => $credentials['password'] ?? '',
'password_exists' => !empty($credentials['password'])
]
]);
}
public function decrypt(Request $request)
{
$request->validate([
'value' => 'required|string',
'app_key' => 'nullable|string',
]);
if ($request->filled('app_key')) {
try {
$rawKey = base64_decode(str_replace('base64:', '', $request->app_key));
$encrypter = new \Illuminate\Encryption\Encrypter($rawKey, 'AES-256-CBC');
$credentials = json_decode($encrypter->decryptString($request->value), true);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'No se pudo desencriptar con el APP_KEY proporcionado',
'error' => $e->getMessage(),
], 422);
}
} else {
$credentials = EncryptionHelper::decryptData($request->value);
}
if (!$credentials) {
return response()->json([
'success' => false,
'message' => 'No se pudo desencriptar el valor proporcionado',
], 422);
}
return response()->json([
'success' => true,
'data' => $credentials,
]);
}
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

@ -7,9 +7,9 @@
/** /**
* Solicitud de login * Solicitud de login
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class LoginRequest extends FormRequest class LoginRequest extends FormRequest
@ -30,8 +30,17 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'email' => ['required', 'email'], 'username' => ['required', 'string'],
'password' => ['required', 'min:8'], 'password' => ['required', 'min:8'],
]; ];
} }
public function messages(): array
{
return [
'username.required' => 'El usuario 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,29 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class ApkStorageRequest extends FormRequest
{
public function authorize()
{
return auth()->user()->HasPermissionTo('apk.create');
}
public function rules()
{
return [
'apk' => 'required|file|max:153600',
];
}
public function messages()
{
return [
'apk.required' => 'El archivo APK es obligatorio',
'apk.file' => 'El archivo debe ser un archivo válido',
'apk.max' => 'El archivo no debe superar los 150MB',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class ApkUpdateRequest extends FormRequest
{
public function authorize()
{
return auth()->user()->HasPermissionTo('apk.edit');
}
public function rules()
{
return [
'file_name' => 'nullable|string|max:255',
'changelog' => 'nullable|string|max:255',
];
}
public function messages()
{
return [
'file_name.string' => 'El nombre del archivo debe ser una cadena de texto',
'file_name.max' => 'El nombre del archivo no debe superar los 255 caracteres',
'changelog.string' => 'El changelog debe ser una cadena de texto',
'changelog.max' => 'El changelog no debe superar los 255 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 auth()->user()->hasPermissionTo('cancellations.cancel_constancia');
}
/**
* 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,43 @@
<?php namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class CatalogCancellationReasonStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'code' => ['required', 'string', 'unique:catalog_cancellation_reasons,code'],
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'applies_to' => ['required', 'in:cancelacion,sustitucion,ambos'],
];
}
public function messages(): array
{
return [
'code.required' => 'El código es obligatorio.',
'code.unique' => 'El código ya existe.',
'name.required' => 'El nombre es obligatorio.',
'applies_to.required' => 'El tipo de aplicación es obligatorio.',
'applies_to.in' => 'El tipo de aplicación debe ser: cancelacion, sustitucion o ambos.',
];
}
public function attributes(): array
{
return [
'code' => 'código',
'name' => 'nombre',
'description' => 'descripción',
'applies_to' => 'aplica a',
];
}
}

View File

@ -0,0 +1,39 @@
<?php namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class CatalogCancellationReasonUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'applies_to' => ['required', 'in:cancelacion,sustitucion,ambos'],
];
}
public function messages(): array
{
return [
'name.required' => 'El nombre es obligatorio.',
'applies_to.required' => 'El tipo de aplicación es obligatorio.',
'applies_to.in' => 'El tipo de aplicación debe ser: cancelacion, sustitucion o ambos.',
];
}
public function attributes(): array
{
return [
'name' => 'nombre',
'description' => 'descripción',
'applies_to' => 'aplica a',
];
}
}

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 auth()->user()->can('catalogs.name_img.create');
}
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 auth()->user()->can('catalogs.name_img.edit');
}
public function rules(): array
{
return [
'names' => ['required'],
];
}
public function messages(): array
{
return [
'names.required' => 'El nombre es requerido',
];
}
}

View File

@ -0,0 +1,38 @@
<?php namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class DeviceStoreRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('devices.create');
}
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' => ['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',
'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.array' => 'Los usuarios autorizados deben ser un array',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class DeviceUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('devices.edit');
}
public function rules(): array
{
return [
'brand' => ['nullable', 'string', 'max:255'],
'serie' => ['nullable', 'string', 'max:255'],
'mac_address' => ['nullable', 'string', '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,48 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ModuleStoreRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('modules.create');
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', Rule::unique('modules', 'name')],
'responsible_id' => ['required', 'exists:users,id'],
'municipality_id' => 'required|exists:municipalities,id',
'address' => ['required', 'string', 'max:255'],
'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',
'name.unique' => 'Ya existe un módulo con ese nombre.',
'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',
'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,49 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ModuleUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('modules.edit');
}
public function rules(): array
{
return [
'name' => ['nullable', 'string', 'max:50', Rule::unique('modules', 'name')->ignore($this->route('module'))],
'municipality_id' => ['nullable', 'integer', 'exists:municipalities,id'],
'responsible_id' => ['nullable', 'integer', 'exists:users,id'],
'address' => ['nullable', 'string', 'max:250'],
'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.unique' => 'Ya existe un módulo con ese nombre.',
'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',
'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;
use Illuminate\Validation\Rule;
class PackageStoreRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('packages.create');
}
public function rules(): array
{
return [
'lot' => ['required', 'string', Rule::unique('packages', 'lot')->where(fn($q) => $q->where('box_number', $this->input('box_number')))],
'box_number' => ['required', 'integer'],
'starting_page' => ['required', 'string', 'regex:/^\d+$/'],
'ending_page' => ['required', 'string', 'regex:/^\d+$/', 'gte:starting_page'],
];
}
public function messages(): array
{
return [
'lot.required' => 'El lote es requerido',
'lot.unique' => 'Ya existe un paquete con ese lote y número de caja.',
'box_number.required' => 'El número de caja es requerido',
'starting_page.required' => 'La página inicial es requerida',
'starting_page.regex' => 'La página inicial debe ser un número',
'ending_page.required' => 'La página final es requerida',
'ending_page.regex' => 'La página final debe ser un número',
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class PackageUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('packages.edit');
}
public function rules(): array
{
return [
'lot' => ['sometimes', 'string'],
'box_number' => ['sometimes', 'integer'],
'starting_page' => ['sometimes', 'string', 'regex:/^\d+$/'],
'ending_page' => ['sometimes', 'string', 'regex:/^\d+$/', '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.regex' => 'La página inicial debe ser un número',
'ending_page.required' => 'La página final es requerida',
'ending_page.regex' => 'La página final debe ser un número',
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
];
}
}

View File

@ -0,0 +1,44 @@
<?php namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class TagStoreRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('tags.create');
}
public function rules(): array
{
return [
'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'],
];
}
public function messages(): array
{
return [
'folio.required' => 'El folio es obligatorio.',
'folio.max' => 'El folio no puede tener más de 8 caracteres.',
'package_id.required' => 'La caja es obligatoria.',
'package_id.exists' => 'La caja seleccionada no existe.',
'tag_number.min' => 'El número de constancia debe tener exactamente 32 caracteres.',
'tag_number.max' => 'El número de constancia debe tener exactamente 32 caracteres.',
'module_id.exists' => 'El módulo seleccionado no existe.',
];
}
public function attributes(): array
{
return [
'folio' => 'folio',
'package_id' => 'caja',
'tag_number' => 'número de constancia',
'module_id' => 'módulo',
];
}
}

View File

@ -0,0 +1,45 @@
<?php namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class TagUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->can('tags.edit');
}
public function rules(): array
{
return [
'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'],
];
}
public function messages(): array
{
return [
'folio.max' => 'El folio no puede tener más de 8 caracteres.',
'tag_number.min' => 'El número de constancia debe tener exactamente 32 caracteres.',
'tag_number.max' => 'El número de constancia debe tener exactamente 32 caracteres.',
'package_id.exists' => 'La caja seleccionada no existe.',
'module_id.exists' => 'El módulo seleccionado no existe.',
'status_id.exists' => 'El estado seleccionado no existe.',
];
}
public function attributes(): array
{
return [
'folio' => 'folio',
'tag_number' => 'número de constancia',
'package_id' => 'caja',
'module_id' => 'módulo',
'status_id' => 'estado',
];
}
}

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_format:d/m/Y'],
'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

@ -21,7 +21,7 @@ class RoleStoreRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return auth()->user()->hasPermissionTo('roles.create'); return auth()->user()->can('roles.create');
} }
/** /**

View File

@ -9,9 +9,9 @@
/** /**
* Actualizar rol * Actualizar rol
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class RoleUpdateRequest extends FormRequest class RoleUpdateRequest extends FormRequest
@ -21,7 +21,7 @@ class RoleUpdateRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return auth()->user()->hasPermissionTo('roles.edit'); return auth()->user()->can('roles.edit');
} }
/** /**
@ -39,8 +39,10 @@ public function rules(): array
*/ */
protected function passedValidation() protected function passedValidation()
{ {
$this->merge([ if(!in_array($this->route('role')->id, [1, 2])) {
'name' => Str::slug($this->description), $this->merge([
]); 'name' => Str::slug($this->description),
]);
}
} }
} }

View File

@ -28,7 +28,15 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'email' => ['required', 'email', 'exists:users,email'] 'username' => ['required', 'string', 'exists:users,username']
];
}
public function messages(): array
{
return [
'username.required' => 'El nombre de usuario es requerido',
'username.exists' => 'El usuario no existe',
]; ];
} }
} }

View File

@ -32,11 +32,11 @@ public function rules(): array
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'paternal' => ['required', 'string', 'max:255'], 'paternal' => ['required', 'string', 'max:255'],
'maternal' => ['required', 'string', 'max:255'], 'maternal' => ['required', 'string', 'max:255'],
'email' => [ 'username' => [
'required', 'required',
'string', 'string',
'email',
'max:255', 'max:255',
'alpha_dash',
Rule::unique('users')->ignore(auth()->user()->id), Rule::unique('users')->ignore(auth()->user()->id),
], ],
'phone' => ['nullable', 'numeric', 'digits:10'], 'phone' => ['nullable', 'numeric', 'digits:10'],

View File

@ -7,9 +7,9 @@
/** /**
* Descripción * Descripción
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class UserActivityRequest extends FormRequest class UserActivityRequest extends FormRequest
@ -19,7 +19,7 @@ class UserActivityRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return true; return auth()->user()->can('activities.index');
} }
/** /**

View File

@ -7,9 +7,9 @@
/** /**
* Almacenar usuario * Almacenar usuario
* *
* @author Moisés Cortés C <moises.cortes@notsoweb.com> * @author Moisés Cortés C <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class UserStoreRequest extends FormRequest class UserStoreRequest extends FormRequest
@ -33,10 +33,19 @@ public function rules(): array
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'paternal' => ['required', 'string', 'max:255'], 'paternal' => ['required', 'string', 'max:255'],
'maternal' => ['required', 'string', 'max:255'], 'maternal' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'username' => ['required', 'string', 'max:255', 'unique:users', 'alpha_dash'],
'phone' => ['nullable', 'numeric', 'digits:10'], 'phone' => ['nullable', 'numeric', 'digits:10'],
'password' => ['required', 'string', 'min:8'], 'password' => ['required', 'string', 'min:8'],
'roles' => ['nullable', 'array'] 'roles' => ['nullable', 'array']
]; ];
} }
public function messages(): array
{
return [
'username.required' => 'El nombre de usuario es requerido',
'username.unique' => 'El nombre de usuario ya está en uso',
'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
];
}
} }

View File

@ -34,9 +34,18 @@ public function rules(): array
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'paternal' => ['required', 'string', 'max:255'], 'paternal' => ['required', 'string', 'max:255'],
'maternal' => ['required', 'string', 'max:255'], 'maternal' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($this->route('user'))], 'username' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('users')->ignore($this->route('user'))],
'phone' => ['nullable', 'numeric', 'digits:10'], 'phone' => ['nullable', 'numeric', 'digits:10'],
'roles' => ['nullable', 'array'] 'roles' => ['nullable', 'array']
]; ];
} }
public function messages(): array
{
return [
'username.required' => 'El nombre de usuario es requerido',
'username.unique' => 'El nombre de usuario ya está en uso',
'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
];
}
} }

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,
]);
}
}
}

32
app/Models/ApkLog.php Normal file
View File

@ -0,0 +1,32 @@
<?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 ApkLog extends Model
{
protected $fillable = [
'apk_version_id',
'downloaded_by',
];
public function apkVersion()
{
return $this->belongsTo(ApkVersion::class, 'apk_version_id');
}
public function downloader()
{
return $this->belongsTo(User::class, 'downloaded_by');
}
}

25
app/Models/ApkVersion.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApkVersion extends Model
{
protected $fillable = [
'file_name',
'uploaded_by',
'changelog',
'path',
];
public function uploader()
{
return $this->belongsTo(User::class, 'uploaded_by');
}
public function logs()
{
return $this->hasMany(ApkLog::class, 'apk_version_id');
}
}

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),
);
}
}

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

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

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

@ -0,0 +1,31 @@
<?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 = [];
public function tags()
{
return $this->hasMany(Tag::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

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

@ -0,0 +1,85 @@
<?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'
);
}
/**
* Filtra registros por módulo del usuario
* Admin y developer pueden ver todos los registros
* Otros usuarios solo ven registros de su módulo
*/
public function scopeForUserModule($query, $user)
{
$isAdminOrDeveloper = $user->hasRole(['admin', 'developer'])
|| $user->hasRole(['admin', 'developer'], 'api')
|| $user->roles()->whereIn('name', ['admin', 'developer'])->exists();
if ($isAdminOrDeveloper) {
return $query;
}
if (is_null($user->module_id)) {
return $query->whereRaw('1 = 0');
}
return $query->where('module_id', $user->module_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
*/ */
@ -19,9 +22,9 @@
/** /**
* Modelo de usuario * Modelo de usuario
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
#[ObservedBy([UserObserver::class])] #[ObservedBy([UserObserver::class])]
@ -42,10 +45,10 @@ class User extends Authenticatable
'name', 'name',
'paternal', 'paternal',
'maternal', 'maternal',
'email', 'username',
'phone', 'phone',
'password', 'password',
'profile_photo_path', 'module_id'
]; ];
/** /**
@ -54,6 +57,7 @@ class User extends Authenticatable
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
'profile_photo_path'
]; ];
/** /**
@ -62,7 +66,6 @@ class User extends Authenticatable
protected function casts(): array protected function casts(): array
{ {
return [ return [
'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
@ -98,7 +101,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 +111,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 +130,41 @@ public function resetPasswords()
{ {
return $this->hasMany(ResetPassword::class); return $this->hasMany(ResetPassword::class);
} }
public function module()
{
return $this->belongsTo(Module::class);
}
/**
* Preguntar si el usuario es desarrollador
*/
public function isDeveloper(): bool
{
return $this->hasRole(Role::find(1));
}
/**
* Preguntar si el usuario es administrador
*/
public function isAdmin(): bool
{
return $this->hasRole(Role::find(2));
}
/**
* Preguntar si el usuario es primario (privilegios elevados)
*/
public function isPrimary(): bool
{
return $this->hasRole(Role::find(1), Role::find(2));
}
/**
* Módulo del cual el usuario es responsable
*/
public function responsibleModule()
{
return $this->hasOne(Module::class, 'responsible_id');
}
} }

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

@ -0,0 +1,61 @@
<?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 = [
'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,74 @@
<?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',
'folio_anterior',
'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 performedBy() {
return $this->belongsTo(User::class, 'performed_by');
}
public function cancellationReason()
{
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
}
public function isInscription()
{
return $this->action_type === 'sustitucion_primera_vez';
}
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

@ -23,7 +23,7 @@ public function created(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -35,7 +35,7 @@ public function updated(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email', key: 'username',
reportChanges: true reportChanges: true
); );
} }
@ -48,7 +48,7 @@ public function deleted(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -60,7 +60,7 @@ public function restored(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -72,7 +72,7 @@ public function forceDeleted(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
} }

View File

@ -11,9 +11,9 @@
/** /**
* Proveedor de servicios de Telescope * Proveedor de servicios de Telescope
* *
* @author Moisés Cortés C. <moises.cortes@notsoweb.com> * @author Moisés Cortés C. <moises.cortes@notsoweb.com>
* *
* @version 1.0.0 * @version 1.0.0
*/ */
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
@ -65,7 +65,7 @@ protected function hideSensitiveRequestDetails(): void
protected function gate(): void protected function gate(): void
{ {
Gate::define('viewTelescope', function (User $user) { Gate::define('viewTelescope', function (User $user) {
return $user->hasRole('developer'); return $user->isDeveloper();
}); });
} }
} }

Some files were not shown because too many files have changed in this diff Show More