Compare commits
No commits in common. "develop" and "main" have entirely different histories.
10
.env.example
10
.env.example
@ -34,9 +34,9 @@ DB_PORT=3306
|
||||
DB_DATABASE=holos-backend
|
||||
DB_USERNAME=notsoweb
|
||||
DB_PASSWORD=
|
||||
DB_ROOT_PASSWORD=
|
||||
PMA_PORT=8081 # Puerto para phpMyAdmin
|
||||
|
||||
REDIS_PORT=6379 # Puerto para Redis
|
||||
NGINX_PORT=8080 # Puerto para Nginx
|
||||
|
||||
SESSION_DRIVER=database
|
||||
@ -75,14 +75,6 @@ AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
# REPUVE FEDERAL
|
||||
REPUVE_FED_BASE_URL=
|
||||
REPUVE_FED_USERNAME=
|
||||
REPUVE_FED_PASSWORD=
|
||||
|
||||
# REPUVE ESTATAL
|
||||
REPUVE_EST_URL=
|
||||
|
||||
REVERB_APP_ID=
|
||||
REVERB_APP_KEY=
|
||||
REVERB_APP_SECRET=
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -8,7 +8,6 @@
|
||||
/public/vendor
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/storage/app/backup
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
@ -25,7 +24,3 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
CLAUDE.md
|
||||
/Docker/QA/.env.qa
|
||||
/Docker/Dev/.env.dev
|
||||
/Docker/Prod/.env.prod
|
||||
@ -1,97 +0,0 @@
|
||||
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}"
|
||||
@ -1,25 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
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
|
||||
@ -1,47 +0,0 @@
|
||||
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"]
|
||||
@ -1,39 +0,0 @@
|
||||
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
|
||||
@ -1,144 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
[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
|
||||
@ -1,130 +0,0 @@
|
||||
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
|
||||
@ -1,47 +0,0 @@
|
||||
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"]
|
||||
@ -1,10 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
isDefault: true
|
||||
jsonData:
|
||||
maxLines: 1000
|
||||
@ -1,39 +0,0 @@
|
||||
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
|
||||
@ -1,144 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
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}"
|
||||
@ -1,25 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
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
|
||||
@ -1,47 +0,0 @@
|
||||
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"]
|
||||
@ -1,10 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
isDefault: true
|
||||
jsonData:
|
||||
maxLines: 1000
|
||||
@ -1,39 +0,0 @@
|
||||
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
|
||||
@ -1,144 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,87 +1,12 @@
|
||||
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 {
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /var/www/repuve-backend-v1/public;
|
||||
root /var/www/golscontrols/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;
|
||||
# Logging
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
# Handle Laravel routes (Front Controller)
|
||||
location / {
|
||||
@ -92,7 +17,7 @@ http {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass repuve-backend:9000;
|
||||
fastcgi_pass golscontrols:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
# Timeouts importantes para evitar errores 500
|
||||
@ -114,25 +39,23 @@ http {
|
||||
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;
|
||||
client_max_body_size 20M;
|
||||
|
||||
# Handle storage files (Laravel storage link)
|
||||
location /storage/ {
|
||||
alias /var/www/repuve-backend-v1/storage/app/public/;
|
||||
location /storage {
|
||||
alias /var/www/golscontrols/storage/app;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /profile {
|
||||
alias /var/www/repuve-backend-v1/storage/app/profile;
|
||||
alias /var/www/golscontrols/storage/app/profile;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /images {
|
||||
alias /var/www/repuve-backend-v1/storage/app/images;
|
||||
alias /var/www/golscontrols/storage/app/images;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
@ -141,4 +64,3 @@ http {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
[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
|
||||
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class BackupCron extends Command
|
||||
{
|
||||
|
||||
protected $signature = 'backup:cron';
|
||||
protected $description = 'Respaldo automático de la base de datos';
|
||||
|
||||
/**
|
||||
* Ejecutar comando
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$filename = 'backup-' . date('Y-m-d_H-i-s') . '.sql';
|
||||
$containerPath = '/tmp/' . $filename;
|
||||
$finalPath = storage_path('app/backup/' . $filename);
|
||||
|
||||
// Asegurar que existe el directorio de backup
|
||||
$backupDir = storage_path('app/backup');
|
||||
if (!file_exists($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
// Crear backup ejecutando mysqldump dentro del contenedor MySQL
|
||||
$dumpCommand = sprintf(
|
||||
'docker exec repuve-backend-v1-mysql-1 sh -c "mysqldump --no-tablespaces -u%s -p%s %s > %s" 2>&1',
|
||||
env('DB_USERNAME'),
|
||||
env('DB_PASSWORD'),
|
||||
env('DB_DATABASE'),
|
||||
$containerPath
|
||||
);
|
||||
exec($dumpCommand, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Error al crear el backup en MySQL');
|
||||
$this->error('Código: ' . $returnCode);
|
||||
if (!empty($output)) {
|
||||
$this->error('Salida: ' . implode("\n", $output));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Copiar del contenedor MySQL a /tmp del host
|
||||
$tempHostPath = '/tmp/' . $filename;
|
||||
$copyCommand = sprintf(
|
||||
'docker cp repuve-backend-v1-mysql-1:%s %s 2>&1',
|
||||
$containerPath,
|
||||
$tempHostPath
|
||||
);
|
||||
exec($copyCommand, $copyOutput, $copyReturnCode);
|
||||
|
||||
if ($copyReturnCode !== 0) {
|
||||
$this->error('Error al copiar el backup');
|
||||
if (!empty($copyOutput)) {
|
||||
$this->error('Salida: ' . implode("\n", $copyOutput));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Mover de /tmp a storage y establecer permisos
|
||||
if (file_exists($tempHostPath)) {
|
||||
rename($tempHostPath, $finalPath);
|
||||
chmod($finalPath, 0644);
|
||||
|
||||
// Limpiar archivo temporal del contenedor MySQL
|
||||
exec('docker exec repuve-backend-v1-mysql-1 rm ' . $containerPath);
|
||||
|
||||
$size = filesize($finalPath);
|
||||
$this->info('Backup creado exitosamente: ' . $filename);
|
||||
$this->info('Tamaño: ' . $this->formatBytes($size));
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
$this->error('Error: el archivo temporal no se creó');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatear bytes a tamaño legible
|
||||
*/
|
||||
private function formatBytes($bytes, $precision = 2)
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= pow(1024, $pow);
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class PadronEstatalException extends \RuntimeException {}
|
||||
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EncryptionHelper
|
||||
{
|
||||
/**
|
||||
* Encrypt the given data.
|
||||
*/
|
||||
public static function encryptData($data)
|
||||
{
|
||||
try{
|
||||
return Crypt::encryptString(json_encode($data));
|
||||
}catch(\Exception $e){
|
||||
throw new \RuntimeException("Error al encriptar los datos: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the given data.
|
||||
*/
|
||||
public static function decryptData($encryptedData)
|
||||
{
|
||||
try{
|
||||
$decrypted = Crypt::decryptString($encryptedData);
|
||||
return json_decode($decrypted, true);
|
||||
}catch(DecryptException $e){
|
||||
Log::error('Error al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
}catch(\Exception $e){
|
||||
Log::error('Error inesperado al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function encryptFields(array $data, array $fields)
|
||||
{
|
||||
foreach ($fields as $field){
|
||||
if(isset($data[$field])){
|
||||
$data[$field] = self::encryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function decryptFields(array $data, array $fields)
|
||||
{
|
||||
foreach ($fields as $field){
|
||||
if(isset($data[$field])){
|
||||
$data[$field] = self::decryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
<?php namespace App\Http\Controllers\Admin;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
@ -13,9 +10,9 @@
|
||||
|
||||
/**
|
||||
* Eventos del usuarios del sistema
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class ActivityController extends Controller
|
||||
@ -27,23 +24,27 @@ public function index(UserActivityRequest $request)
|
||||
{
|
||||
$filters = $request->all();
|
||||
|
||||
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at')
|
||||
->when(isset($filters['user']) && !empty($filters['user']), function ($query) use ($filters) {
|
||||
$query->where('user_id', $filters['user']);
|
||||
})
|
||||
->when(isset($filters['search']) && !empty($filters['search']), function ($query) use ($filters) {
|
||||
$query->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");
|
||||
})
|
||||
->when(isset($filters['end_date']) && !empty($filters['end_date']), function ($query) use ($filters) {
|
||||
$query->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
|
||||
});
|
||||
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at');
|
||||
|
||||
if(isset($filters['user']) && !empty($filters['user'])){
|
||||
$model->where('user_id', $filters['user']);
|
||||
}
|
||||
|
||||
if(isset($filters['search']) && !empty($filters['search'])){
|
||||
$model->where('event', 'like', '%'.$filters['search'].'%');
|
||||
}
|
||||
|
||||
if(isset($filters['start_date']) && !empty($filters['start_date'])){
|
||||
$model->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
|
||||
}
|
||||
|
||||
if(isset($filters['end_date']) && !empty($filters['end_date'])){
|
||||
$model->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'models' =>
|
||||
$model->orderBy('created_at', 'desc')
|
||||
'models' =>
|
||||
$model->orderBy('created_at', 'desc')
|
||||
->paginate(config('app.pagination'))
|
||||
]);
|
||||
}
|
||||
|
||||
@ -5,26 +5,17 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PermissionType;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Tipos de permisos
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class PermissionTypeController extends Controller implements HasMiddleware
|
||||
class PermissionTypeController extends Controller
|
||||
{
|
||||
|
||||
public static function middleware(): array
|
||||
{
|
||||
return [
|
||||
self::can('roles.index', ['all', 'allWithPermissions']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todo
|
||||
*/
|
||||
@ -40,13 +31,8 @@ public function all()
|
||||
*/
|
||||
public function allWithPermissions()
|
||||
{
|
||||
$hidden = ['Actividad', 'Cargar APK'];
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'models' => PermissionType::with('permissions')
|
||||
->whereNotIn('name', $hidden)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
'models' => PermissionType::with('permissions')->orderBy('name')->get()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Roles\RoleStoreRequest;
|
||||
use App\Http\Requests\Roles\RoleUpdateRequest;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use App\Models\Role;
|
||||
use App\Supports\QuerySupport;
|
||||
use Illuminate\Http\Request;
|
||||
@ -15,31 +14,19 @@
|
||||
|
||||
/**
|
||||
* Roles del sistema
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class RoleController extends Controller implements HasMiddleware
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$model = Role::where('id', '!=','1')->orderBy('description');
|
||||
$model = Role::orderBy('description');
|
||||
|
||||
QuerySupport::queryByKey($model, request(), 'name');
|
||||
|
||||
@ -54,13 +41,9 @@ public function index()
|
||||
*/
|
||||
public function store(RoleStoreRequest $request)
|
||||
{
|
||||
$model = Role::create($request->all());
|
||||
Role::create($request->all());
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Rol creado exitosamente',
|
||||
'id' => $model->id,
|
||||
'name' => $model->name,
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,11 +71,6 @@ public function update(RoleUpdateRequest $request, 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();
|
||||
|
||||
return ApiResponse::OK->response();
|
||||
@ -103,12 +81,8 @@ public function destroy(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([
|
||||
'permissions' => $permissions->values()
|
||||
'permissions' => $role->permissions
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -12,52 +12,29 @@
|
||||
use App\Supports\QuerySupport;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
|
||||
/**
|
||||
* Controlador de usuarios
|
||||
*
|
||||
*
|
||||
* Permite la administración de los usuarios en general.
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class UserController extends Controller implements HasMiddleware
|
||||
class UserController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Middleware
|
||||
*/
|
||||
public static function middleware(): array
|
||||
{
|
||||
return [
|
||||
self::can('users.index', ['index']),
|
||||
self::can('users.destroy', ['destroy']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$users = User::whereDoesntHave('roles', function ($query) {
|
||||
$query->where('name', 'developer');
|
||||
})->orderBy('name');
|
||||
$users = User::orderBy('name');
|
||||
|
||||
QuerySupport::queryByKeys($users, ['name', 'username']);
|
||||
QuerySupport::queryByKeys($users, ['name', 'email']);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'users' => $users->select([
|
||||
'id',
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'username',
|
||||
'module_id',
|
||||
'deleted_at'
|
||||
])->paginate(config('app.pagination'))
|
||||
'models' => $users->paginate(config('app.pagination'))
|
||||
]);
|
||||
}
|
||||
|
||||
@ -72,10 +49,7 @@ public function store(UserStoreRequest $request)
|
||||
$user->roles()->sync($request->roles);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Usuario actualizado exitosamente',
|
||||
'user' => $user->load(['module', 'roles']),
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,14 +69,7 @@ public function update(UserUpdateRequest $request, User $user)
|
||||
{
|
||||
$user->update($request->all());
|
||||
|
||||
if ($request->has('roles')) {
|
||||
$user->roles()->sync($request->roles);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Usuario actualizado exitosamente',
|
||||
'user' => $user->load(['module', 'roles']),
|
||||
]);
|
||||
return ApiResponse::OK->response();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +152,7 @@ public function activity(UserActivityRequest $request, User $user)
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'models' =>
|
||||
'models' =>
|
||||
$model->orderBy('created_at', 'desc')
|
||||
->paginate(config('app.pagination'))
|
||||
]);
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
<?php namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controllers\Middleware;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
/**
|
||||
* Controlador base
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
abstract class Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Evaluar permisos de un usuario
|
||||
*/
|
||||
public static function can(string $permission, array $methods): Middleware
|
||||
{
|
||||
return new Middleware("permission:{$permission}", only: $methods);
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
@ -23,11 +23,11 @@ class AuthController extends Controller
|
||||
*/
|
||||
public function login(LoginRequest $request)
|
||||
{
|
||||
$user = User::where('username', $request->get('username'))->first();
|
||||
$user = User::where('email', $request->get('email'))->first();
|
||||
|
||||
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
||||
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||
'username' => ['Usuario no valido']
|
||||
'email' => ['Usuario no valido']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -1,144 +0,0 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@ -1,395 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\CancelConstanciaRequest;
|
||||
use App\Models\CatalogCancellationReason;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use App\Models\Record;
|
||||
use App\Models\Tag;
|
||||
use App\Models\TagCancellationLog;
|
||||
use App\Models\VehicleTagLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,297 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Requests\Repuve\DeviceStoreRequest;
|
||||
use App\Http\Requests\Repuve\DeviceUpdateRequest;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceModule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
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
@ -1,643 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Requests\Repuve\VehicleStoreRequest;
|
||||
use App\Models\CatalogNameImg;
|
||||
use App\Models\Vehicle;
|
||||
use App\Models\Record;
|
||||
use App\Models\Owner;
|
||||
use App\Models\File;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VehicleTagLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<?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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,269 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\ModuleStoreRequest;
|
||||
use App\Http\Requests\Repuve\ModuleUpdateRequest;
|
||||
use App\Models\Module;
|
||||
use 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
<?php namespace App\Http\Controllers\Repuve;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Models\Municipality;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class MunicipalityController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$municipalities = Municipality::orderBy('id', 'ASC')->get();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $municipalities,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|unique:municipalities,code',
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
$municipality = Municipality::create([
|
||||
'code' => $request->input('code'),
|
||||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'data' => $municipality,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$municipality = Municipality::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'code' => 'required|unique:municipalities,code,' . $municipality->id,
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
$municipality->update([
|
||||
'code' => $request->input('code'),
|
||||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $municipality,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$municipality = Municipality::findOrFail($id);
|
||||
$municipality->delete();
|
||||
|
||||
return ApiResponse::NO_CONTENT->response();
|
||||
}
|
||||
}
|
||||
@ -1,488 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Repuve;
|
||||
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Repuve\PackageStoreRequest;
|
||||
use App\Http\Requests\Repuve\PackageUpdateRequest;
|
||||
use App\Models\CatalogTagStatus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Database\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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,700 +0,0 @@
|
||||
<?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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,591 +0,0 @@
|
||||
<?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
@ -16,9 +16,9 @@
|
||||
|
||||
/**
|
||||
* Controlador de sesiones
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class LoginController extends Controller
|
||||
@ -28,11 +28,11 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function login(LoginRequest $request)
|
||||
{
|
||||
$user = User::where('username', $request->get('username'))->first();
|
||||
$user = User::where('email', $request->get('email'))->first();
|
||||
|
||||
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
||||
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||
'username' => ['Credenciales inválidas']
|
||||
'email' => ['Usuario no valido']
|
||||
]);
|
||||
}
|
||||
|
||||
@ -56,35 +56,27 @@ public function logout()
|
||||
|
||||
/**
|
||||
* Contraseña olvidada
|
||||
* Nota: Sin email, el reset se maneja por token directo
|
||||
*/
|
||||
public function forgotPassword(ForgotRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$user = User::where('username', $data['username'])->first();
|
||||
|
||||
if (!$user) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'username' => ['Usuario no encontrado']
|
||||
]);
|
||||
}
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
try {
|
||||
$token = $this->generateToken($user);
|
||||
|
||||
// Sin email, retornar el token directamente (para uso administrativo)
|
||||
$user->notify(new ForgotPasswordNotification($token));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'is_generated' => true,
|
||||
'token' => $token,
|
||||
'message' => 'Token generado. Válido por 15 minutos.',
|
||||
'is_sent' => true
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
Log::channel('mail')->info("Username: {$data['username']}");
|
||||
Log::channel('mail')->info("Email: {$data['email']}");
|
||||
Log::channel('mail')->error($th->getMessage());
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'is_generated' => false,
|
||||
'is_sent' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -95,7 +87,7 @@ public function forgotPassword(ForgotRequest $request)
|
||||
public function resetPassword(ResetPasswordRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
|
||||
$model = ResetPassword::with('user')->where('token', $data['token'])->first();
|
||||
|
||||
if(!$model){
|
||||
@ -150,4 +142,4 @@ private function deleteToken($token)
|
||||
{
|
||||
ResetPassword::where('token', $token)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
<?php namespace App\Http\Controllers\System;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
use App\Enums\SettingTypeEk;
|
||||
use Illuminate\Http\Request;
|
||||
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
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,9 @@
|
||||
|
||||
/**
|
||||
* Solicitud de login
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class LoginRequest extends FormRequest
|
||||
@ -30,17 +30,8 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['required', 'string'],
|
||||
'email' => ['required', 'email'],
|
||||
'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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CancelConstanciaRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FileStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre es obligatorio',
|
||||
'file.required' => 'El archivo es obligatorio',
|
||||
'file.mimes' => 'El archivo debe ser de tipo: jpeg, png, jpg',
|
||||
'file.max' => 'El archivo no debe superar los 10MB',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VehicleStoreRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'folio' => ['required', 'string', 'max:50'],
|
||||
'tag_number' => ['required', 'string'],
|
||||
'placa' => ['required', 'string', 'max:30'],
|
||||
'telefono' => ['required', 'string', 'max:11'],
|
||||
'files' => ['nullable', 'array', 'min:1'],
|
||||
'files.*' => ['file', 'mimes:jpeg,png,jpg', 'max:10240'],
|
||||
'name_id' => ['nullable', 'array', 'min:1'],
|
||||
'name_id.*' => ['nullable', 'integer', 'exists:catalog_name_img,id']
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'folio.required' => 'El folio es requerido',
|
||||
'folio.string' => 'El folio debe ser una cadena de texto',
|
||||
'tag_number.required' => 'El tag_number es requerido',
|
||||
'placa.required' => 'La placa es requerida',
|
||||
'placa.string' => 'La placa debe ser una cadena de texto',
|
||||
'telefono.required' => 'El teléfono es requerido',
|
||||
'telefono.max' => 'El teléfono no debe superar los 10 caracteres',
|
||||
'files.array' => 'Los archivos deben ser un array',
|
||||
'files.*.file' => 'Cada elemento debe ser un archivo válido',
|
||||
'files.*.mimes' => 'Los archivos deben ser de tipo: jpeg, png, jpg, pdf',
|
||||
'files.*.max' => 'Cada archivo no debe superar los 10MB',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Repuve;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VehicleUpdateRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'folio' => 'nullable|string|max:50|unique:records,folio,' . $this->route('id'),
|
||||
// --- DATOS DEL VEHÍCULO ---
|
||||
'vehicle.placa' => 'nullable|string|max:20',
|
||||
'vehicle.marca' => 'nullable|string|max:100',
|
||||
'vehicle.linea' => 'nullable|string|max:100',
|
||||
'vehicle.sublinea' => 'nullable|string|max:100',
|
||||
'vehicle.modelo' => 'nullable|string',
|
||||
'vehicle.color' => 'nullable|string|max:50',
|
||||
'vehicle.numero_motor' => 'nullable|string|max:50',
|
||||
'vehicle.clase_veh' => 'nullable|string|max:50',
|
||||
'vehicle.tipo_servicio' => 'nullable|string|max:50',
|
||||
'vehicle.rfv' => 'nullable|string|max:50',
|
||||
'vehicle.rfc' => 'nullable|string|max:13',
|
||||
'vehicle.ofcexpedicion' => 'nullable|string|max:100',
|
||||
'vehicle.fechaexpedicion' => ['nullable', 'date_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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ class RoleStoreRequest extends FormRequest
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->can('roles.create');
|
||||
return auth()->user()->hasPermissionTo('roles.create');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
|
||||
/**
|
||||
* Actualizar rol
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class RoleUpdateRequest extends FormRequest
|
||||
@ -21,7 +21,7 @@ class RoleUpdateRequest extends FormRequest
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->can('roles.edit');
|
||||
return auth()->user()->hasPermissionTo('roles.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,10 +39,8 @@ public function rules(): array
|
||||
*/
|
||||
protected function passedValidation()
|
||||
{
|
||||
if(!in_array($this->route('role')->id, [1, 2])) {
|
||||
$this->merge([
|
||||
'name' => Str::slug($this->description),
|
||||
]);
|
||||
}
|
||||
$this->merge([
|
||||
'name' => Str::slug($this->description),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,15 +28,7 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'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',
|
||||
'email' => ['required', 'email', 'exists:users,email']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,11 +32,11 @@ public function rules(): array
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'paternal' => ['required', 'string', 'max:255'],
|
||||
'maternal' => ['required', 'string', 'max:255'],
|
||||
'username' => [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'alpha_dash',
|
||||
Rule::unique('users')->ignore(auth()->user()->id),
|
||||
],
|
||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class UserActivityRequest extends FormRequest
|
||||
@ -19,7 +19,7 @@ class UserActivityRequest extends FormRequest
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->can('activities.index');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
|
||||
/**
|
||||
* Almacenar usuario
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class UserStoreRequest extends FormRequest
|
||||
@ -33,19 +33,10 @@ public function rules(): array
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'paternal' => ['required', 'string', 'max:255'],
|
||||
'maternal' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users', 'alpha_dash'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||
'password' => ['required', 'string', 'min:8'],
|
||||
'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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,18 +34,9 @@ public function rules(): array
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'paternal' => ['required', 'string', 'max:255'],
|
||||
'maternal' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('users')->ignore($this->route('user'))],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($this->route('user'))],
|
||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||
'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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
<?php namespace App\Jobs;
|
||||
|
||||
use App\Models\Error;
|
||||
use App\Models\Record;
|
||||
use App\Services\RepuveService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
class ProcessRepuveResponse implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 2;
|
||||
public int $timeout = 250;
|
||||
public array $backoff = [30];
|
||||
|
||||
/**
|
||||
* Crear instancia del trabajo
|
||||
*/
|
||||
public function __construct(
|
||||
public int $recordId,
|
||||
public array $responseData
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecutar el trabajo
|
||||
*/
|
||||
public function handle(RepuveService $repuveService): void
|
||||
{
|
||||
$record = Record::findOrFail($this->recordId);
|
||||
|
||||
Log::info('ProcessRepuveResponse: Enviando inscripción a REPUVE Nacional...', [
|
||||
'niv' => $this->responseData['niv'] ?? 'N/A',
|
||||
'placa' => $this->responseData['placa'] ?? 'N/A',
|
||||
]);
|
||||
|
||||
$apiResponse = $repuveService->inscribirVehiculo($this->responseData);
|
||||
|
||||
Log::info('ProcessRepuveResponse: Respuesta recibida de REPUVE', [
|
||||
'has_error' => $apiResponse['has_error'],
|
||||
'error_code' => $apiResponse['error_code'] ?? null,
|
||||
'timestamp' => $apiResponse['timestamp'] ?? null,
|
||||
]);
|
||||
|
||||
if($apiResponse['has_error']){
|
||||
$error = Error::where('code', $apiResponse['error_code'])->first();
|
||||
|
||||
Log::error('ProcessRepuveResponse: Error en respuesta REPUVE', [
|
||||
'error_code' => $apiResponse['error_code'],
|
||||
'error_message' => $apiResponse['error_message'] ?? 'Sin mensaje',
|
||||
'error_found_in_db' => $error ? 'Sí' : 'No',
|
||||
]);
|
||||
|
||||
$record->update([
|
||||
'error_id' => $error?->id,
|
||||
'api_response' => $apiResponse,
|
||||
'error_occurred_at' => now(),
|
||||
]);
|
||||
|
||||
Log::warning('ProcessRepuveResponse: Record actualizado con error', [
|
||||
'record_id' => $record->id,
|
||||
'error_id' => $error?->id,
|
||||
]);
|
||||
} else {
|
||||
$record->update([
|
||||
'error_id' => null,
|
||||
'api_response' => $apiResponse,
|
||||
'error_occurred_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::critical('ProcessRepuveResponse: Job FALLÓ después de todos los intentos', [
|
||||
'record_id' => $this->recordId,
|
||||
'exception_class' => get_class($exception),
|
||||
'exception_message' => $exception->getMessage(),
|
||||
'exception_file' => $exception->getFile(),
|
||||
'exception_line' => $exception->getLine(),
|
||||
'attempts' => $this->attempts(),
|
||||
]);
|
||||
|
||||
$record = Record::find($this->recordId);
|
||||
if($record){
|
||||
Log::info('ProcessRepuveResponse: Buscando error genérico código -1');
|
||||
|
||||
$error = Error::where('code', '-1')->first();
|
||||
|
||||
if(!$error){
|
||||
Log::warning('ProcessRepuveResponse: Error código -1 NO encontrado en BD');
|
||||
}
|
||||
|
||||
$record->update([
|
||||
'error_id' => $error?->id,
|
||||
'api_response' => [
|
||||
'has_error' => true,
|
||||
'error_message' => $exception->getMessage(),
|
||||
],
|
||||
'error_occurred_at' => now(),
|
||||
]);
|
||||
|
||||
Log::error('ProcessRepuveResponse: Record actualizado con error crítico', [
|
||||
'record_id' => $record->id,
|
||||
'error_id' => $error?->id,
|
||||
]);
|
||||
} else {
|
||||
Log::error('ProcessRepuveResponse: Record NO encontrado', [
|
||||
'record_id' => $this->recordId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class 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');
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CatalogCancellationReason extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'applies_to'
|
||||
];
|
||||
|
||||
/**
|
||||
* Obtener razones para cancelación
|
||||
*/
|
||||
public function scopeForCancellation($query)
|
||||
{
|
||||
return $query->whereIn('applies_to', ['cancelacion', 'ambos']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener razones para sustitución
|
||||
*/
|
||||
public function scopeForSubstitution($query)
|
||||
{
|
||||
return $query->whereIn('applies_to', ['sustitucion', 'ambos']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs que usan esta razón
|
||||
*/
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class, 'cancellation_reason_id');
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CatalogNameImg extends Model
|
||||
{
|
||||
protected $table = 'catalog_name_img';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
];
|
||||
|
||||
public function files()
|
||||
{
|
||||
return $this->hasMany(File::class, 'name_id');
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Catálogo de estatus de tags
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class CatalogTagStatus extends Model
|
||||
{
|
||||
// Constantes de códigos de estatus
|
||||
const CODE_AVAILABLE = 'available';
|
||||
const CODE_ASSIGNED = 'assigned';
|
||||
const CODE_CANCELLED = 'cancelled';
|
||||
|
||||
protected $table = 'catalog_tag_status';
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags que tienen este estatus
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(Tag::class, 'status_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para obtener solo estatus activos
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para buscar por código
|
||||
*/
|
||||
public function scopeByCode($query, string $code)
|
||||
{
|
||||
return $query->where('code', $code);
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Device extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'brand',
|
||||
'serie',
|
||||
'mac_address',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function modules()
|
||||
{
|
||||
return $this->belongsToMany(Module::class, 'device_module')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function deviceModules()
|
||||
{
|
||||
return $this->hasMany(DeviceModule::class);
|
||||
}
|
||||
|
||||
public function activeModules()
|
||||
{
|
||||
return $this->belongsToMany(Module::class, 'device_module')
|
||||
->wherePivot('status', true)
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DeviceModule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'device_module';
|
||||
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'module_id',
|
||||
'user_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function device()
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(Module::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Error extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'type'
|
||||
];
|
||||
|
||||
public function records()
|
||||
{
|
||||
return $this->hasMany(Record::class);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name_id',
|
||||
'path',
|
||||
'md5',
|
||||
'observations',
|
||||
'record_id',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'url',
|
||||
];
|
||||
|
||||
|
||||
public function record()
|
||||
{
|
||||
return $this->belongsTo(Record::class);
|
||||
}
|
||||
|
||||
public function catalogName()
|
||||
{
|
||||
return $this->belongsTo(CatalogNameImg::class, 'name_id');
|
||||
}
|
||||
|
||||
public function url(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => Storage::disk('public')->url($this->path),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Module extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'responsible_id',
|
||||
'municipality_id',
|
||||
'address',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'longitude' => 'decimal:8',
|
||||
'latitude' => 'decimal:8',
|
||||
'status' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function municipality()
|
||||
{
|
||||
return $this->belongsTo(Municipality::class);
|
||||
}
|
||||
|
||||
public function tags(){
|
||||
return $this->hasMany(Tag::class, 'module_id');
|
||||
}
|
||||
|
||||
public function devices()
|
||||
{
|
||||
return $this->belongsTo(Device::class, 'device_module')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function deviceModules()
|
||||
{
|
||||
return $this->hasMany(DeviceModule::class);
|
||||
}
|
||||
|
||||
public function activeDevices()
|
||||
{
|
||||
return $this->belongsToMany(Device::class, 'device_module')
|
||||
->wherePivot('status', true)
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function responsible()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'responsible_id');
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class, 'module_id');
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Municipality extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
];
|
||||
|
||||
public function modules()
|
||||
{
|
||||
return $this->hasMany(Module::class);
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Owner extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'rfc',
|
||||
'curp',
|
||||
'address',
|
||||
'tipopers',
|
||||
'pasaporte',
|
||||
'licencia',
|
||||
'ent_fed',
|
||||
'munic',
|
||||
'callep',
|
||||
'num_ext',
|
||||
'num_int',
|
||||
'colonia',
|
||||
'cp',
|
||||
'telefono',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'full_name',
|
||||
];
|
||||
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => trim("{$this->name} {$this->paternal} {$this->maternal}")
|
||||
);
|
||||
}
|
||||
|
||||
public function vehicles()
|
||||
{
|
||||
return $this->hasMany(Vehicle::class);
|
||||
}
|
||||
|
||||
public function municipality()
|
||||
{
|
||||
return $this->belongsTo(Municipality::class, 'munic', 'code');
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Package extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'lot',
|
||||
'box_number',
|
||||
'starting_page',
|
||||
'ending_page',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [];
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(Tag::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Record extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'folio',
|
||||
'vehicle_id',
|
||||
'user_id',
|
||||
'module_id',
|
||||
'error_id',
|
||||
'api_response',
|
||||
'error_occurred_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'api_response' => 'array',
|
||||
'error_occurred_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function vehicle()
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function files()
|
||||
{
|
||||
return $this->hasMany(File::class);
|
||||
}
|
||||
|
||||
public function error()
|
||||
{
|
||||
return $this->belongsTo(Error::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(Module::class);
|
||||
}
|
||||
|
||||
public function vehicleTagLog()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
VehicleTagLog::class,
|
||||
Vehicle::class,
|
||||
'id',
|
||||
'vehicle_id',
|
||||
'vehicle_id',
|
||||
'id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScanHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'scan_history';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'tag_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relación con User
|
||||
* Un escaneo pertenece a un usuario
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con Tag
|
||||
* Un escaneo pertenece a una etiqueta
|
||||
*/
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// Constantes de status
|
||||
const STATUS_AVAILABLE = 'available';
|
||||
const STATUS_ASSIGNED = 'assigned';
|
||||
const STATUS_CANCELLED = 'cancelled';
|
||||
const STATUS_DAMAGED = 'damaged';
|
||||
|
||||
protected $fillable = [
|
||||
'folio',
|
||||
'tag_number',
|
||||
'vehicle_id',
|
||||
'package_id',
|
||||
'module_id',
|
||||
'status_id',
|
||||
];
|
||||
|
||||
public function vehicle()
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function package()
|
||||
{
|
||||
return $this->belongsTo(Package::class);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(CatalogTagStatus::class, 'status_id');
|
||||
}
|
||||
|
||||
public function module(){
|
||||
return $this->belongsTo(Module::class, 'module_id');
|
||||
}
|
||||
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class);
|
||||
}
|
||||
|
||||
public function scanHistories()
|
||||
{
|
||||
return $this->hasMany(ScanHistory::class);
|
||||
}
|
||||
|
||||
public function cancellationLogs()
|
||||
{
|
||||
return $this->hasMany(TagCancellationLog::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar tag como asignado a un vehículo
|
||||
*/
|
||||
public function markAsAssigned(int $vehicleId, string $folio): void
|
||||
{
|
||||
$statusAssigned = CatalogTagStatus::where('code', self::STATUS_ASSIGNED)->first();
|
||||
|
||||
$this->update([
|
||||
'vehicle_id' => $vehicleId,
|
||||
'folio' => $folio,
|
||||
'status_id' => $statusAssigned->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar tag como cancelado
|
||||
*/
|
||||
public function markAsCancelled(): void
|
||||
{
|
||||
$statusCancelled = CatalogTagStatus::where('code', self::STATUS_CANCELLED)->first();
|
||||
|
||||
$this->update([
|
||||
'status_id' => $statusCancelled->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar tag como dañado
|
||||
*/
|
||||
public function markAsDamaged(): void
|
||||
{
|
||||
$statusDamaged = CatalogTagStatus::where('code', self::STATUS_DAMAGED)->first();
|
||||
|
||||
$this->update([
|
||||
'status_id' => $statusDamaged->id,
|
||||
'vehicle_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está disponible
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_AVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está asignado
|
||||
*/
|
||||
public function isAssigned(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_ASSIGNED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está cancelado
|
||||
*/
|
||||
public function isCancelled(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el tag está dañado
|
||||
*/
|
||||
public function isDamaged(): bool
|
||||
{
|
||||
return $this->status->code === self::STATUS_DAMAGED;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Log de cancelación de tags no asignados
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class TagCancellationLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'tag_cancellation_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'tag_id',
|
||||
'cancellation_reason_id',
|
||||
'cancellation_observations',
|
||||
'cancellation_at',
|
||||
'cancelled_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'cancellation_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
// Relaciones
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
}
|
||||
|
||||
public function cancellationReason()
|
||||
{
|
||||
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
|
||||
}
|
||||
|
||||
public function cancelledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
@ -22,9 +19,9 @@
|
||||
|
||||
/**
|
||||
* Modelo de usuario
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
#[ObservedBy([UserObserver::class])]
|
||||
@ -45,10 +42,10 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'username',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'module_id'
|
||||
'profile_photo_path',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -57,7 +54,6 @@ class User extends Authenticatable
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'profile_photo_path'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -66,6 +62,7 @@ class User extends Authenticatable
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
@ -101,7 +98,7 @@ public function reports()
|
||||
public function fullName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
|
||||
get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,7 +108,7 @@ public function fullName(): Attribute
|
||||
public function lastName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->paternal . ' ' . $this->maternal,
|
||||
get: fn () => $this->paternal . ' ' . $this->maternal,
|
||||
);
|
||||
}
|
||||
|
||||
@ -130,41 +127,4 @@ public function resetPasswords()
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Vehicle extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'vehicle';
|
||||
|
||||
protected $fillable = [
|
||||
'placa',
|
||||
'niv',
|
||||
'marca',
|
||||
'linea',
|
||||
'sublinea',
|
||||
'modelo',
|
||||
'color',
|
||||
'numero_motor',
|
||||
'clase_veh',
|
||||
'tipo_servicio',
|
||||
'rfv',
|
||||
'ofcexpedicion',
|
||||
'fechaexpedicion',
|
||||
'tipo_veh',
|
||||
'numptas',
|
||||
'observac',
|
||||
'cve_vehi',
|
||||
'nrpv',
|
||||
'tipo_mov',
|
||||
'owner_id',
|
||||
'reporte_robo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'reporte_robo' => 'boolean',
|
||||
];
|
||||
|
||||
public function owner()
|
||||
{
|
||||
return $this->belongsTo(Owner::class);
|
||||
}
|
||||
|
||||
public function records()
|
||||
{
|
||||
return $this->hasMany(Record::class);
|
||||
}
|
||||
|
||||
public function tag()
|
||||
{
|
||||
return $this->hasOne(Tag::class);
|
||||
}
|
||||
|
||||
public function vehicleTagLogs()
|
||||
{
|
||||
return $this->hasMany(VehicleTagLog::class);
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VehicleTagLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'vehicle_tags_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'vehicle_id',
|
||||
'tag_id',
|
||||
'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';
|
||||
}
|
||||
|
||||
}
|
||||
@ -23,7 +23,7 @@ public function created(User $user): void
|
||||
UserEvent::report(
|
||||
model: $user,
|
||||
event: __FUNCTION__,
|
||||
key: 'username'
|
||||
key: 'email'
|
||||
);
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ public function updated(User $user): void
|
||||
UserEvent::report(
|
||||
model: $user,
|
||||
event: __FUNCTION__,
|
||||
key: 'username',
|
||||
key: 'email',
|
||||
reportChanges: true
|
||||
);
|
||||
}
|
||||
@ -48,7 +48,7 @@ public function deleted(User $user): void
|
||||
UserEvent::report(
|
||||
model: $user,
|
||||
event: __FUNCTION__,
|
||||
key: 'username'
|
||||
key: 'email'
|
||||
);
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ public function restored(User $user): void
|
||||
UserEvent::report(
|
||||
model: $user,
|
||||
event: __FUNCTION__,
|
||||
key: 'username'
|
||||
key: 'email'
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ public function forceDeleted(User $user): void
|
||||
UserEvent::report(
|
||||
model: $user,
|
||||
event: __FUNCTION__,
|
||||
key: 'username'
|
||||
key: 'email'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@
|
||||
|
||||
/**
|
||||
* Proveedor de servicios de Telescope
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
||||
@ -65,7 +65,7 @@ protected function hideSensitiveRequestDetails(): void
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewTelescope', function (User $user) {
|
||||
return $user->isDeveloper();
|
||||
return $user->hasRole('developer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user