Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebd91ad070 | |||
| b17b6608d3 | |||
| be300d449d | |||
|
|
c0b263eb87 | ||
|
|
3a3c0a2ebf | ||
|
|
828dd8fa21 | ||
| 767e7abf5b | |||
|
|
633198e5ae | ||
|
|
f5c4fce98a | ||
|
|
c3cad386aa | ||
|
|
153c296ea1 | ||
|
|
2410f13320 | ||
|
|
2237572d1f | ||
|
|
7510123626 | ||
|
|
c0d0e8dd86 | ||
|
|
1566c891a5 | ||
|
|
15d42dbcec | ||
|
|
3ca44ea26b | ||
|
|
53f451c54c | ||
|
|
b8f210478e | ||
|
|
f7941af3cf | ||
|
|
19af2f4bef | ||
|
|
5ec3e6d52a | ||
|
|
2c5307fa5b | ||
|
|
fc897f5130 | ||
|
|
28d008bf54 | ||
|
|
886d9cf3a2 | ||
|
|
4784bbdb9e | ||
|
|
3e7d381f15 | ||
| e701d5348a | |||
| 3bc216d28e | |||
| 09d80e6726 | |||
|
|
4c417be38c | ||
|
|
ad6b19e9dd | ||
|
|
0faabb3026 | ||
|
|
5bad287ef4 | ||
|
|
31746867b8 | ||
| 69371a0088 | |||
| e846f00767 | |||
| 0f9aefa131 | |||
|
|
feb240698f | ||
| aeebda6faa | |||
|
|
f04dbccedb | ||
|
|
de9d801d50 | ||
|
|
43269ca04a | ||
| 2093ff7538 | |||
|
|
557fe6858c | ||
|
|
79c1043f7a | ||
|
|
2717176373 | ||
|
|
04c9fe2d5a | ||
|
|
23ba2a03f9 | ||
|
|
d65711106f | ||
|
|
b1f05e6267 | ||
|
|
64196c9d5b | ||
|
|
392b155367 | ||
| 43c1400e7c | |||
| c725072291 | |||
| b602687233 | |||
|
|
ebae87f97c | ||
|
|
e4419f1a50 | ||
|
|
69727724d3 | ||
|
|
6eeba6f9fe | ||
|
|
6106afbecf | ||
|
|
6d27b9a818 | ||
|
|
63a2a3a338 | ||
|
|
977d5a0420 | ||
|
|
ea4e3fc607 | ||
|
|
2411ff1eab | ||
|
|
1d7afe1b9a | ||
|
|
927c46aa2e | ||
| ee9b265582 | |||
| 50e0cff497 | |||
|
|
974efc1c9c | ||
|
|
22f7e0226e | ||
|
|
0a7b46c0bc | ||
|
|
ce7e1f998e | ||
|
|
c0225a69e2 | ||
|
|
7c530595f4 | ||
|
|
46367238f5 | ||
|
|
2ca6950751 | ||
|
|
4f9b4a6098 | ||
|
|
e9fd55aa3b | ||
|
|
30c0b8f587 | ||
|
|
d3bd94e158 | ||
|
|
1b2522c4f0 | ||
|
|
3bde46589b | ||
|
|
88ef9d272c | ||
|
|
44ef2f9306 | ||
|
|
908ba8aaf1 | ||
|
|
a18f028a3d | ||
|
|
672b7dd735 | ||
|
|
525bcc0db7 | ||
| 1c6cb99187 | |||
| 89989c6fe4 | |||
|
|
44d568e6c1 | ||
|
|
0741d1830f | ||
|
|
0ec75bc0ba | ||
| c4643c60d4 | |||
| 685ad3d3a8 | |||
|
|
859596d858 | ||
|
|
8865000919 | ||
|
|
20a7e3beda | ||
|
|
18e3bdefdb | ||
| ebc4f3c546 | |||
| 901957ff66 | |||
|
|
b68d360274 | ||
|
|
dcac63f953 | ||
|
|
a8f84783cd | ||
|
|
fd44d9c310 | ||
|
|
0b498fb33f | ||
|
|
ffed4cbde5 | ||
|
|
6425aa5cc1 | ||
|
|
ffc0fd1fd9 | ||
|
|
ff7b4a7d3d | ||
|
|
456768e1b5 | ||
|
|
4004585dc8 | ||
|
|
2db309a203 | ||
| e0649e85ef | |||
| 5a144e282f | |||
|
|
3dee92ab1e | ||
|
|
ca5b1ad8ed | ||
|
|
1784e3065b | ||
|
|
553752fcfc | ||
|
|
c56e3b1435 | ||
|
|
74dedc32df | ||
|
|
118f5ef868 | ||
| baf3961036 | |||
| 8c6afe40bc | |||
|
|
cb16c0b91c | ||
|
|
5826a2c26c | ||
|
|
0be583a088 | ||
|
|
96f938a279 | ||
|
|
dd2298a5c8 | ||
|
|
d9bc4886ce | ||
|
|
3079826f0f | ||
|
|
3e48f8d6db | ||
|
|
df252f55f2 | ||
|
|
b5779e5bdb | ||
|
|
8e2db75ad2 | ||
|
|
70f3679ba4 | ||
|
|
6af33e4503 | ||
| 02220407ff | |||
|
|
80c0076a92 | ||
|
|
9b37989f0a | ||
|
|
5efce1978b | ||
|
|
fbf55ae67c | ||
|
|
48eaf28151 | ||
|
|
b25db1a0d4 | ||
|
|
ac57bdcbc4 | ||
| 92b64887bd | |||
| 9ebc3f4167 | |||
| 3d6649c504 | |||
| c9e5cb86c8 | |||
| fd727ca45f | |||
| 36865e7cef | |||
| 75889becaf | |||
|
|
d8ec98cd7c | ||
| 2d060f9909 | |||
|
|
cf7cfdb821 | ||
|
|
ebc64a6a8e | ||
|
|
684315eb18 | ||
|
|
a305c82956 | ||
|
|
e57bb79762 | ||
|
|
f25901ed9d | ||
|
|
975c6863ff | ||
| dfb60806cb | |||
| 90f943291e | |||
|
|
6388410153 | ||
|
|
cac2263a4f | ||
|
|
c7f1b46714 | ||
|
|
78ac5ab75e | ||
| ef864c4753 | |||
|
|
4c6dccf056 | ||
|
|
48fec8fdd8 | ||
|
|
f05e1679a0 | ||
|
|
ee7947ae29 | ||
|
|
3640fc8a13 | ||
|
|
1060435f52 | ||
|
|
d995e27a39 | ||
|
|
16361e0a27 | ||
|
|
12986d51cc | ||
|
|
27cbc965b5 | ||
|
|
82b7a179f5 | ||
|
|
551ef38ffc | ||
|
|
9348e03010 | ||
|
|
9a4f70baa2 | ||
| 83065b1cc4 | |||
|
|
dd2b3211db | ||
|
|
7a5bf3a2a0 | ||
|
|
fae8979532 | ||
|
|
7ad6f87b17 | ||
|
|
906299ac10 | ||
|
|
d7761088dd | ||
|
|
41c85a8ade | ||
|
|
3a25257dc5 | ||
| 1d69c5ee7c | |||
|
|
99c2767877 | ||
| 843d3404e9 | |||
|
|
e97d124967 | ||
|
|
2fc21be5a2 | ||
|
|
c4935d5298 | ||
| beeaf481a0 | |||
|
|
aa2266f34d | ||
|
|
d281e505e0 | ||
| bf0346dabf | |||
|
|
de0ac7a3aa | ||
| 71788ddd2e | |||
| f3dd6cd32c | |||
| 4e1c5855af | |||
|
|
92bf244fe6 | ||
|
|
a66b8d77d6 | ||
|
|
2b448644c2 | ||
|
|
2bb87e7daf |
10
.env.example
10
.env.example
@ -34,9 +34,9 @@ DB_PORT=3306
|
|||||||
DB_DATABASE=holos-backend
|
DB_DATABASE=holos-backend
|
||||||
DB_USERNAME=notsoweb
|
DB_USERNAME=notsoweb
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
DB_ROOT_PASSWORD=
|
||||||
PMA_PORT=8081 # Puerto para phpMyAdmin
|
PMA_PORT=8081 # Puerto para phpMyAdmin
|
||||||
|
|
||||||
REDIS_PORT=6379 # Puerto para Redis
|
|
||||||
NGINX_PORT=8080 # Puerto para Nginx
|
NGINX_PORT=8080 # Puerto para Nginx
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
@ -75,6 +75,14 @@ AWS_DEFAULT_REGION=us-east-1
|
|||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
# REPUVE FEDERAL
|
||||||
|
REPUVE_FED_BASE_URL=
|
||||||
|
REPUVE_FED_USERNAME=
|
||||||
|
REPUVE_FED_PASSWORD=
|
||||||
|
|
||||||
|
# REPUVE ESTATAL
|
||||||
|
REPUVE_EST_URL=
|
||||||
|
|
||||||
REVERB_APP_ID=
|
REVERB_APP_ID=
|
||||||
REVERB_APP_KEY=
|
REVERB_APP_KEY=
|
||||||
REVERB_APP_SECRET=
|
REVERB_APP_SECRET=
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,7 @@
|
|||||||
/public/vendor
|
/public/vendor
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/pail
|
/storage/pail
|
||||||
|
/storage/app/backup
|
||||||
/vendor
|
/vendor
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
@ -24,3 +25,4 @@ yarn-error.log
|
|||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
25
Docker/Dev/config.alloy
Normal file
25
Docker/Dev/config.alloy
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
logging {
|
||||||
|
level = "info"
|
||||||
|
format = "logfmt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descubrir archivos de log
|
||||||
|
local.file_match "laravel_logs" {
|
||||||
|
path_targets = [
|
||||||
|
{ __path__ = "/var/log/repuve/padron-estatal.log", job = "padron_estatal", env = "dev" },
|
||||||
|
{ __path__ = "/var/log/repuve/repuve-nacional.log", job = "repuve_nacional", env = "dev" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer los archivos
|
||||||
|
loki.source.file "laravel_reader" {
|
||||||
|
targets = local.file_match.laravel_logs.targets
|
||||||
|
forward_to = [loki.write.local.receiver]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar a Loki
|
||||||
|
loki.write "local" {
|
||||||
|
endpoint {
|
||||||
|
url = "http://loki:3100/loki/api/v1/push"
|
||||||
|
}
|
||||||
|
}
|
||||||
124
Docker/Dev/docker-compose.yml
Normal file
124
Docker/Dev/docker-compose.yml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
name: repuve-backend-dev
|
||||||
|
services:
|
||||||
|
repuve-backend:
|
||||||
|
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-network
|
||||||
|
mem_limit: 512M
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
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-network
|
||||||
|
mem_limit: 256m
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- repuve-backend
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
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}:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- repuve-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
|
||||||
|
networks:
|
||||||
|
- repuve-network
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- repuve-backend
|
||||||
|
- loki
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
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-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- "3000: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-network
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- loki
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
driver: local
|
||||||
|
loki_data:
|
||||||
|
driver: local
|
||||||
|
grafana_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
repuve-network:
|
||||||
|
driver: bridge
|
||||||
43
Docker/Dev/dockerfile
Normal file
43
Docker/Dev/dockerfile
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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 \
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
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 ["php-fpm"]
|
||||||
10
Docker/Dev/grafana/provisioning/datasources/loki.yml
Normal file
10
Docker/Dev/grafana/provisioning/datasources/loki.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
url: http://loki:3100
|
||||||
|
isDefault: true
|
||||||
|
jsonData:
|
||||||
|
maxLines: 1000
|
||||||
29
Docker/Dev/loki-config.yml
Normal file
29
Docker/Dev/loki-config.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
|
||||||
|
common:
|
||||||
|
path_prefix: /loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /loki/chunks
|
||||||
|
rules_directory: /loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-10-24
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
reject_old_samples: false
|
||||||
25
Docker/Prod/config.alloy
Normal file
25
Docker/Prod/config.alloy
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
logging {
|
||||||
|
level = "warn"
|
||||||
|
format = "logfmt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descubrir archivos de log
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Docker/Prod/docker-compose.yml
Normal file
119
Docker/Prod/docker-compose.yml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
name: repuve-backend-prod
|
||||||
|
services:
|
||||||
|
repuve-backend:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: Docker/Prod/dockerfile
|
||||||
|
working_dir: /var/www/repuve-backend-v1
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- 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-network
|
||||||
|
mem_limit: 512M
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${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-network
|
||||||
|
mem_limit: 256m
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- repuve-backend
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_USER: ${DB_USERNAME}
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- repuve-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:
|
||||||
|
- "127.0.0.1:12345:12345"
|
||||||
|
volumes:
|
||||||
|
- ./config.alloy:/etc/alloy/config.alloy
|
||||||
|
- ../../storage/logs:/var/log/repuve:ro
|
||||||
|
networks:
|
||||||
|
- repuve-network
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- repuve-backend
|
||||||
|
- loki
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3100:3100"
|
||||||
|
volumes:
|
||||||
|
- ./loki-config.yml:/etc/loki/local-config.yaml
|
||||||
|
- loki_data:/loki
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
networks:
|
||||||
|
- repuve-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||||
|
networks:
|
||||||
|
- repuve-network
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- loki
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
driver: local
|
||||||
|
loki_data:
|
||||||
|
driver: local
|
||||||
|
grafana_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
repuve-network:
|
||||||
|
driver: bridge
|
||||||
40
Docker/Prod/dockerfile
Normal file
40
Docker/Prod/dockerfile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
FROM php:8.3-fpm-alpine
|
||||||
|
|
||||||
|
WORKDIR /var/www/repuve-backend-v1
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
curl \
|
||||||
|
libpng-dev \
|
||||||
|
oniguruma-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
libzip-dev \
|
||||||
|
openssl \
|
||||||
|
bash \
|
||||||
|
libreoffice \
|
||||||
|
ttf-dejavu \
|
||||||
|
&& 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 --no-dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY entrypoint-prod.sh /usr/local/bin/entrypoint-prod.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint-prod.sh
|
||||||
|
|
||||||
|
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-prod.sh"]
|
||||||
|
CMD ["php-fpm"]
|
||||||
10
Docker/Prod/grafana/provisioning/datasources/loki.yml
Normal file
10
Docker/Prod/grafana/provisioning/datasources/loki.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
url: http://loki:3100
|
||||||
|
isDefault: true
|
||||||
|
jsonData:
|
||||||
|
maxLines: 1000
|
||||||
29
Docker/Prod/loki-config.yml
Normal file
29
Docker/Prod/loki-config.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
|
||||||
|
common:
|
||||||
|
path_prefix: /loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /loki/chunks
|
||||||
|
rules_directory: /loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-10-24
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
reject_old_samples: false
|
||||||
69
Docker/nginx/default.conf
Normal file
69
Docker/nginx/default.conf
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/repuve-backend-v1/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
# Logging con formatos forenses (definidos en nginx.conf principal)
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
access_log /var/log/nginx/access.log forensic_main;
|
||||||
|
|
||||||
|
# Handle Laravel routes (Front Controller)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle PHP files
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_pass repuve-backend:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
|
||||||
|
# Timeouts importantes para evitar errores 500
|
||||||
|
fastcgi_read_timeout 300;
|
||||||
|
fastcgi_connect_timeout 300;
|
||||||
|
fastcgi_send_timeout 300;
|
||||||
|
|
||||||
|
# Carga los parámetros por defecto
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
# Parámetros críticos para Laravel
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
fastcgi_param HTTP_HOST $http_host;
|
||||||
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
|
fastcgi_param HTTP_PROXY "";
|
||||||
|
|
||||||
|
# Añadir Request ID al backend para tracking
|
||||||
|
fastcgi_param HTTP_X_REQUEST_ID $request_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
# Handle storage files (Laravel storage link)
|
||||||
|
location /storage {
|
||||||
|
alias /var/www/repuve-backend-v1/storage/app/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /profile {
|
||||||
|
alias /var/www/repuve-backend-v1/storage/app/profile;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /images {
|
||||||
|
alias /var/www/repuve-backend-v1/storage/app/images;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Denegar acceso a archivos ocultos como .htaccess
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Docker/nginx/nginx-main.conf
Normal file
76
Docker/nginx/nginx-main.conf
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
# Log de errores con máximo nivel de detalle
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
|
||||||
|
log_format forensic_main
|
||||||
|
'$remote_addr - $remote_user [$time_local] '
|
||||||
|
'"$request" $status $body_bytes_sent '
|
||||||
|
'"$http_referer" "$http_user_agent" '
|
||||||
|
'"$http_x_forwarded_for" "$http_x_real_ip" '
|
||||||
|
'rt=$request_time ' # Tiempo total de la petición
|
||||||
|
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
|
||||||
|
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
|
||||||
|
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
|
||||||
|
'cs=$upstream_cache_status ' # Estado de caché
|
||||||
|
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
|
||||||
|
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
|
||||||
|
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
|
||||||
|
'conn=$connection ' # ID de conexión
|
||||||
|
'conn_reqs=$connection_requests ' # Peticiones por conexión
|
||||||
|
'pipe=$pipe ' # Pipelining (y/n)
|
||||||
|
'host="$host" ' # Host solicitado
|
||||||
|
'server_name="$server_name" '
|
||||||
|
'scheme="$scheme" '
|
||||||
|
'request_method="$request_method" '
|
||||||
|
'request_uri="$request_uri" '
|
||||||
|
'server_port="$server_port" '
|
||||||
|
'http_version="$server_protocol" '
|
||||||
|
'bytes_sent=$bytes_sent ' # Total bytes enviados
|
||||||
|
'request_length=$request_length ' # Tamaño de la petición
|
||||||
|
'req_id="$request_id"'; # ID único por petición
|
||||||
|
|
||||||
|
# Formato adicional para headers sensibles / seguridad
|
||||||
|
log_format forensic_headers
|
||||||
|
'$remote_addr [$time_local] req_id="$request_id" '
|
||||||
|
'Authorization="$http_authorization" '
|
||||||
|
'Cookie="$http_cookie" '
|
||||||
|
'Content-Type="$content_type" '
|
||||||
|
'Content-Length="$content_length" '
|
||||||
|
'Accept="$http_accept" '
|
||||||
|
'Accept-Language="$http_accept_language" '
|
||||||
|
'Accept-Encoding="$http_accept_encoding" '
|
||||||
|
'Origin="$http_origin" '
|
||||||
|
'Sec-Fetch-Site="$http_sec_fetch_site" '
|
||||||
|
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
|
||||||
|
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
|
||||||
|
'X-Custom-Header="$http_x_custom_header"';
|
||||||
|
|
||||||
|
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
|
||||||
|
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
|
||||||
|
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
|
||||||
|
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server_tokens off; # No revelar versión en respuestas (buena práctica)
|
||||||
|
|
||||||
|
# Añadir request_id único a cada petición
|
||||||
|
add_header X-Request-ID $request_id always;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
@ -1,12 +1,87 @@
|
|||||||
server {
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
# Log de errores con máximo nivel de detalle
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# ─── FORMATO DE LOG FORENSE EXTENDIDO ───────────────────────────────────────
|
||||||
|
log_format forensic_main
|
||||||
|
'$remote_addr - $remote_user [$time_local] '
|
||||||
|
'"$request" $status $body_bytes_sent '
|
||||||
|
'"$http_referer" "$http_user_agent" '
|
||||||
|
'"$http_x_forwarded_for" "$http_x_real_ip" '
|
||||||
|
'rt=$request_time ' # Tiempo total de la petición
|
||||||
|
'uct="$upstream_connect_time" ' # Tiempo de conexión upstream
|
||||||
|
'uht="$upstream_header_time" ' # Tiempo de cabeceras upstream
|
||||||
|
'urt="$upstream_response_time" ' # Tiempo de respuesta upstream
|
||||||
|
'cs=$upstream_cache_status ' # Estado de caché
|
||||||
|
'ssl_protocol="$ssl_protocol" ' # Protocolo SSL usado
|
||||||
|
'ssl_cipher="$ssl_cipher" ' # Cifrado SSL
|
||||||
|
'ssl_session_id="$ssl_session_id" ' # ID sesión TLS (rastreo)
|
||||||
|
'conn=$connection ' # ID de conexión
|
||||||
|
'conn_reqs=$connection_requests ' # Peticiones por conexión
|
||||||
|
'pipe=$pipe ' # Pipelining (y/n)
|
||||||
|
'host="$host" ' # Host solicitado
|
||||||
|
'server_name="$server_name" '
|
||||||
|
'scheme="$scheme" '
|
||||||
|
'request_method="$request_method" '
|
||||||
|
'request_uri="$request_uri" '
|
||||||
|
'server_port="$server_port" '
|
||||||
|
'http_version="$server_protocol" '
|
||||||
|
'bytes_sent=$bytes_sent ' # Total bytes enviados
|
||||||
|
'request_length=$request_length ' # Tamaño de la petición
|
||||||
|
'req_id="$request_id"'; # ID único por petición
|
||||||
|
|
||||||
|
# Formato adicional para headers sensibles / seguridad
|
||||||
|
log_format forensic_headers
|
||||||
|
'$remote_addr [$time_local] req_id="$request_id" '
|
||||||
|
'Authorization="$http_authorization" '
|
||||||
|
'Cookie="$http_cookie" '
|
||||||
|
'Content-Type="$content_type" '
|
||||||
|
'Content-Length="$content_length" '
|
||||||
|
'Accept="$http_accept" '
|
||||||
|
'Accept-Language="$http_accept_language" '
|
||||||
|
'Accept-Encoding="$http_accept_encoding" '
|
||||||
|
'Origin="$http_origin" '
|
||||||
|
'Sec-Fetch-Site="$http_sec_fetch_site" '
|
||||||
|
'Sec-Fetch-Mode="$http_sec_fetch_mode" '
|
||||||
|
'Sec-Fetch-Dest="$http_sec_fetch_dest" '
|
||||||
|
'X-Custom-Header="$http_x_custom_header"';
|
||||||
|
|
||||||
|
# ─── ARCHIVOS DE LOG ────────────────────────────────────────────────────────
|
||||||
|
access_log /var/log/nginx/access.log forensic_main buffer=16k flush=1s;
|
||||||
|
access_log /var/log/nginx/headers.log forensic_headers buffer=16k flush=1s;
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
|
||||||
|
# ─── OPCIONES GENERALES ─────────────────────────────────────────────────────
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server_tokens off; # No revelar versión en respuestas (buena práctica)
|
||||||
|
|
||||||
|
# Añadir request_id único a cada petición
|
||||||
|
add_header X-Request-ID $request_id always;
|
||||||
|
|
||||||
|
# ─── SERVER BLOCK LARAVEL ───────────────────────────────────────────────────
|
||||||
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /var/www/golscontrols/public;
|
root /var/www/repuve-backend-v1/public;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|
||||||
# Logging
|
# Logging con formatos forenses (definidos en nginx.conf principal)
|
||||||
error_log /var/log/nginx/error.log;
|
error_log /var/log/nginx/error.log debug;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log forensic_main;
|
||||||
|
|
||||||
# Handle Laravel routes (Front Controller)
|
# Handle Laravel routes (Front Controller)
|
||||||
location / {
|
location / {
|
||||||
@ -17,7 +92,7 @@ server {
|
|||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
fastcgi_pass golscontrols:9000;
|
fastcgi_pass repuve-backend:9000;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
|
|
||||||
# Timeouts importantes para evitar errores 500
|
# Timeouts importantes para evitar errores 500
|
||||||
@ -39,23 +114,25 @@ server {
|
|||||||
fastcgi_param HTTP_HOST $http_host;
|
fastcgi_param HTTP_HOST $http_host;
|
||||||
fastcgi_param HTTPS $https if_not_empty;
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
fastcgi_param HTTP_PROXY "";
|
fastcgi_param HTTP_PROXY "";
|
||||||
|
|
||||||
|
# Añadir Request ID al backend para tracking
|
||||||
|
fastcgi_param HTTP_X_REQUEST_ID $request_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_max_body_size 20M;
|
client_max_body_size 150M;
|
||||||
|
|
||||||
# Handle storage files (Laravel storage link)
|
# Handle storage files (Laravel storage link)
|
||||||
location /storage {
|
location /storage/ {
|
||||||
alias /var/www/golscontrols/storage/app;
|
alias /var/www/repuve-backend-v1/storage/app/public/;
|
||||||
try_files $uri =404;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /profile {
|
location /profile {
|
||||||
alias /var/www/golscontrols/storage/app/profile;
|
alias /var/www/repuve-backend-v1/storage/app/profile;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /images {
|
location /images {
|
||||||
alias /var/www/golscontrols/storage/app/images;
|
alias /var/www/repuve-backend-v1/storage/app/images;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,3 +141,4 @@ server {
|
|||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
95
app/Console/Commands/BackupCron.php
Normal file
95
app/Console/Commands/BackupCron.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class BackupCron extends Command
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $signature = 'backup:cron';
|
||||||
|
protected $description = 'Respaldo automático de la base de datos';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecutar comando
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$filename = 'backup-' . date('Y-m-d_H-i-s') . '.sql';
|
||||||
|
$containerPath = '/tmp/' . $filename;
|
||||||
|
$finalPath = storage_path('app/backup/' . $filename);
|
||||||
|
|
||||||
|
// Asegurar que existe el directorio de backup
|
||||||
|
$backupDir = storage_path('app/backup');
|
||||||
|
if (!file_exists($backupDir)) {
|
||||||
|
mkdir($backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear backup ejecutando mysqldump dentro del contenedor MySQL
|
||||||
|
$dumpCommand = sprintf(
|
||||||
|
'docker exec repuve-backend-v1-mysql-1 sh -c "mysqldump --no-tablespaces -u%s -p%s %s > %s" 2>&1',
|
||||||
|
env('DB_USERNAME'),
|
||||||
|
env('DB_PASSWORD'),
|
||||||
|
env('DB_DATABASE'),
|
||||||
|
$containerPath
|
||||||
|
);
|
||||||
|
exec($dumpCommand, $output, $returnCode);
|
||||||
|
|
||||||
|
if ($returnCode !== 0) {
|
||||||
|
$this->error('Error al crear el backup en MySQL');
|
||||||
|
$this->error('Código: ' . $returnCode);
|
||||||
|
if (!empty($output)) {
|
||||||
|
$this->error('Salida: ' . implode("\n", $output));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copiar del contenedor MySQL a /tmp del host
|
||||||
|
$tempHostPath = '/tmp/' . $filename;
|
||||||
|
$copyCommand = sprintf(
|
||||||
|
'docker cp repuve-backend-v1-mysql-1:%s %s 2>&1',
|
||||||
|
$containerPath,
|
||||||
|
$tempHostPath
|
||||||
|
);
|
||||||
|
exec($copyCommand, $copyOutput, $copyReturnCode);
|
||||||
|
|
||||||
|
if ($copyReturnCode !== 0) {
|
||||||
|
$this->error('Error al copiar el backup');
|
||||||
|
if (!empty($copyOutput)) {
|
||||||
|
$this->error('Salida: ' . implode("\n", $copyOutput));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mover de /tmp a storage y establecer permisos
|
||||||
|
if (file_exists($tempHostPath)) {
|
||||||
|
rename($tempHostPath, $finalPath);
|
||||||
|
chmod($finalPath, 0644);
|
||||||
|
|
||||||
|
// Limpiar archivo temporal del contenedor MySQL
|
||||||
|
exec('docker exec repuve-backend-v1-mysql-1 rm ' . $containerPath);
|
||||||
|
|
||||||
|
$size = filesize($finalPath);
|
||||||
|
$this->info('Backup creado exitosamente: ' . $filename);
|
||||||
|
$this->info('Tamaño: ' . $this->formatBytes($size));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
$this->error('Error: el archivo temporal no se creó');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatear bytes a tamaño legible
|
||||||
|
*/
|
||||||
|
private function formatBytes($bytes, $precision = 2)
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$bytes = max($bytes, 0);
|
||||||
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||||
|
$pow = min($pow, count($units) - 1);
|
||||||
|
$bytes /= pow(1024, $pow);
|
||||||
|
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/Exceptions/PadronEstatalException.php
Normal file
5
app/Exceptions/PadronEstatalException.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class PadronEstatalException extends \RuntimeException {}
|
||||||
59
app/Helpers/EncryptionHelper.php
Normal file
59
app/Helpers/EncryptionHelper.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class EncryptionHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Encrypt the given data.
|
||||||
|
*/
|
||||||
|
public static function encryptData($data)
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
return Crypt::encryptString(json_encode($data));
|
||||||
|
}catch(\Exception $e){
|
||||||
|
throw new \RuntimeException("Error al encriptar los datos: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the given data.
|
||||||
|
*/
|
||||||
|
public static function decryptData($encryptedData)
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
$decrypted = Crypt::decryptString($encryptedData);
|
||||||
|
return json_decode($decrypted, true);
|
||||||
|
}catch(DecryptException $e){
|
||||||
|
Log::error('Error al desencriptar los datos: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}catch(\Exception $e){
|
||||||
|
Log::error('Error inesperado al desencriptar los datos: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function encryptFields(array $data, array $fields)
|
||||||
|
{
|
||||||
|
foreach ($fields as $field){
|
||||||
|
if(isset($data[$field])){
|
||||||
|
$data[$field] = self::encryptData($data[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decryptFields(array $data, array $fields)
|
||||||
|
{
|
||||||
|
foreach ($fields as $field){
|
||||||
|
if(isset($data[$field])){
|
||||||
|
$data[$field] = self::decryptData($data[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
<?php namespace App\Http\Controllers\Admin;
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
*/
|
*/
|
||||||
@ -10,9 +13,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Eventos del usuarios del sistema
|
* Eventos del usuarios del sistema
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class ActivityController extends Controller
|
class ActivityController extends Controller
|
||||||
@ -24,27 +27,23 @@ public function index(UserActivityRequest $request)
|
|||||||
{
|
{
|
||||||
$filters = $request->all();
|
$filters = $request->all();
|
||||||
|
|
||||||
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at');
|
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at')
|
||||||
|
->when(isset($filters['user']) && !empty($filters['user']), function ($query) use ($filters) {
|
||||||
if(isset($filters['user']) && !empty($filters['user'])){
|
$query->where('user_id', $filters['user']);
|
||||||
$model->where('user_id', $filters['user']);
|
})
|
||||||
}
|
->when(isset($filters['search']) && !empty($filters['search']), function ($query) use ($filters) {
|
||||||
|
$query->where('event', 'like', '%' . $filters['search'] . '%');
|
||||||
if(isset($filters['search']) && !empty($filters['search'])){
|
})
|
||||||
$model->where('event', 'like', '%'.$filters['search'].'%');
|
->when(isset($filters['start_date']) && !empty($filters['start_date']), function ($query) use ($filters) {
|
||||||
}
|
$query->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
|
||||||
|
})
|
||||||
if(isset($filters['start_date']) && !empty($filters['start_date'])){
|
->when(isset($filters['end_date']) && !empty($filters['end_date']), function ($query) use ($filters) {
|
||||||
$model->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
|
$query->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
|
||||||
}
|
});
|
||||||
|
|
||||||
if(isset($filters['end_date']) && !empty($filters['end_date'])){
|
|
||||||
$model->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'models' =>
|
'models' =>
|
||||||
$model->orderBy('created_at', 'desc')
|
$model->orderBy('created_at', 'desc')
|
||||||
->paginate(config('app.pagination'))
|
->paginate(config('app.pagination'))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,17 +5,26 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\PermissionType;
|
use App\Models\PermissionType;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tipos de permisos
|
* Tipos de permisos
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class PermissionTypeController extends Controller
|
class PermissionTypeController extends Controller implements HasMiddleware
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('roles.index', ['all', 'allWithPermissions']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listar todo
|
* Listar todo
|
||||||
*/
|
*/
|
||||||
@ -31,8 +40,13 @@ public function all()
|
|||||||
*/
|
*/
|
||||||
public function allWithPermissions()
|
public function allWithPermissions()
|
||||||
{
|
{
|
||||||
|
$hidden = ['Actividad', 'Cargar APK'];
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'models' => PermissionType::with('permissions')->orderBy('name')->get()
|
'models' => PermissionType::with('permissions')
|
||||||
|
->whereNotIn('name', $hidden)
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Roles\RoleStoreRequest;
|
use App\Http\Requests\Roles\RoleStoreRequest;
|
||||||
use App\Http\Requests\Roles\RoleUpdateRequest;
|
use App\Http\Requests\Roles\RoleUpdateRequest;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use App\Supports\QuerySupport;
|
use App\Supports\QuerySupport;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -14,19 +15,31 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Roles del sistema
|
* Roles del sistema
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class RoleController extends Controller
|
class RoleController extends Controller implements HasMiddleware
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Middleware
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('roles.index', ['index', 'show']),
|
||||||
|
self::can('roles.destroy', ['destroy']),
|
||||||
|
self::can('roles.permissions', ['permissions', 'updatePermissions']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listar
|
* Listar
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$model = Role::orderBy('description');
|
$model = Role::where('id', '!=','1')->orderBy('description');
|
||||||
|
|
||||||
QuerySupport::queryByKey($model, request(), 'name');
|
QuerySupport::queryByKey($model, request(), 'name');
|
||||||
|
|
||||||
@ -41,9 +54,13 @@ public function index()
|
|||||||
*/
|
*/
|
||||||
public function store(RoleStoreRequest $request)
|
public function store(RoleStoreRequest $request)
|
||||||
{
|
{
|
||||||
Role::create($request->all());
|
$model = Role::create($request->all());
|
||||||
|
|
||||||
return ApiResponse::OK->response();
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Rol creado exitosamente',
|
||||||
|
'id' => $model->id,
|
||||||
|
'name' => $model->name,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +88,11 @@ public function update(RoleUpdateRequest $request, Role $role)
|
|||||||
*/
|
*/
|
||||||
public function destroy(Role $role)
|
public function destroy(Role $role)
|
||||||
{
|
{
|
||||||
|
if (in_array($role->id, [1, 2])) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'No se puede eliminar este rol'
|
||||||
|
]);
|
||||||
|
}
|
||||||
$role->delete();
|
$role->delete();
|
||||||
|
|
||||||
return ApiResponse::OK->response();
|
return ApiResponse::OK->response();
|
||||||
@ -81,8 +103,12 @@ public function destroy(Role $role)
|
|||||||
*/
|
*/
|
||||||
public function permissions(Role $role)
|
public function permissions(Role $role)
|
||||||
{
|
{
|
||||||
|
$permissions = $role->id === 2
|
||||||
|
? $role->permissions->filter(fn($p) => !str_starts_with($p->name, 'activities.'))
|
||||||
|
: $role->permissions;
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'permissions' => $role->permissions
|
'permissions' => $permissions->values()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,29 +12,52 @@
|
|||||||
use App\Supports\QuerySupport;
|
use App\Supports\QuerySupport;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlador de usuarios
|
* Controlador de usuarios
|
||||||
*
|
*
|
||||||
* Permite la administración de los usuarios en general.
|
* Permite la administración de los usuarios en general.
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class UserController extends Controller
|
class UserController extends Controller implements HasMiddleware
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('users.index', ['index']),
|
||||||
|
self::can('users.destroy', ['destroy']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listar
|
* Listar
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$users = User::orderBy('name');
|
$users = User::whereDoesntHave('roles', function ($query) {
|
||||||
|
$query->where('name', 'developer');
|
||||||
|
})->orderBy('name');
|
||||||
|
|
||||||
QuerySupport::queryByKeys($users, ['name', 'email']);
|
QuerySupport::queryByKeys($users, ['name', 'username']);
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'models' => $users->paginate(config('app.pagination'))
|
'users' => $users->select([
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'paternal',
|
||||||
|
'maternal',
|
||||||
|
'username',
|
||||||
|
'module_id',
|
||||||
|
'deleted_at'
|
||||||
|
])->paginate(config('app.pagination'))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +72,10 @@ public function store(UserStoreRequest $request)
|
|||||||
$user->roles()->sync($request->roles);
|
$user->roles()->sync($request->roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse::OK->response();
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Usuario actualizado exitosamente',
|
||||||
|
'user' => $user->load(['module', 'roles']),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +95,14 @@ public function update(UserUpdateRequest $request, User $user)
|
|||||||
{
|
{
|
||||||
$user->update($request->all());
|
$user->update($request->all());
|
||||||
|
|
||||||
return ApiResponse::OK->response();
|
if ($request->has('roles')) {
|
||||||
|
$user->roles()->sync($request->roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Usuario actualizado exitosamente',
|
||||||
|
'user' => $user->load(['module', 'roles']),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +185,7 @@ public function activity(UserActivityRequest $request, User $user)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'models' =>
|
'models' =>
|
||||||
$model->orderBy('created_at', 'desc')
|
$model->orderBy('created_at', 'desc')
|
||||||
->paginate(config('app.pagination'))
|
->paginate(config('app.pagination'))
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
<?php namespace App\Http\Controllers;
|
<?php namespace App\Http\Controllers;
|
||||||
/**
|
|
||||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
use Illuminate\Routing\Controllers\Middleware;
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlador base
|
* Controlador base
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
abstract class Controller
|
abstract class Controller
|
||||||
{
|
{
|
||||||
//
|
|
||||||
|
/**
|
||||||
|
* Evaluar permisos de un usuario
|
||||||
|
*/
|
||||||
|
public static function can(string $permission, array $methods): Middleware
|
||||||
|
{
|
||||||
|
return new Middleware("permission:{$permission}", only: $methods);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,11 +23,11 @@ class AuthController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function login(LoginRequest $request)
|
public function login(LoginRequest $request)
|
||||||
{
|
{
|
||||||
$user = User::where('email', $request->get('email'))->first();
|
$user = User::where('username', $request->get('username'))->first();
|
||||||
|
|
||||||
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
||||||
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
'email' => ['Usuario no valido']
|
'username' => ['Usuario no valido']
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
144
app/Http/Controllers/Repuve/AppController.php
Normal file
144
app/Http/Controllers/Repuve/AppController.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Repuve\ApkStorageRequest;
|
||||||
|
use App\Http\Requests\Repuve\ApkUpdateRequest;
|
||||||
|
use App\Models\ApkLog;
|
||||||
|
use App\Models\ApkVersion;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
class AppController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Middleware
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('apk.index', ['index']),
|
||||||
|
self::can('apk.create', ['store']),
|
||||||
|
self::can('apk.edit', ['update']),
|
||||||
|
self::can('apk.destroy', ['destroy']),
|
||||||
|
self::can('apk.download', ['download']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listar versiones: actual, 2 anteriores y las no disponibles
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$latestId = ApkVersion::latest()->value('id');
|
||||||
|
|
||||||
|
$versions = ApkVersion::with('uploader:id,name')
|
||||||
|
->withCount('logs')
|
||||||
|
->latest()
|
||||||
|
->get()
|
||||||
|
->map(function ($version) use ($latestId) {
|
||||||
|
$version->is_current = $version->id === $latestId;
|
||||||
|
$version->available = $version->id === $latestId;
|
||||||
|
return $version;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'current' => $versions->where('is_current', true)->first(),
|
||||||
|
'recent' => $versions->where('is_current', false)->take(2)->values(),
|
||||||
|
'unavailable' => $versions->where('is_current', false)->skip(2)->values(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subir nueva versión del APK
|
||||||
|
*/
|
||||||
|
public function store(ApkStorageRequest $request)
|
||||||
|
{
|
||||||
|
$file = $request->file('apk');
|
||||||
|
|
||||||
|
$fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk';
|
||||||
|
|
||||||
|
// Eliminar APK anterior del storage
|
||||||
|
foreach (Storage::disk('public')->files('apk') as $existing) {
|
||||||
|
Storage::disk('public')->delete($existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $file->storeAs('apk', $fileName, 'public');
|
||||||
|
|
||||||
|
ApkVersion::create([
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'path' => $path,
|
||||||
|
'changelog' => $request->input('changelog'),
|
||||||
|
'uploaded_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'APK subido correctamente',
|
||||||
|
'file' => $fileName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar nombre y/o changelog de una versión
|
||||||
|
*/
|
||||||
|
public function update(ApkUpdateRequest $request, ApkVersion $app)
|
||||||
|
{
|
||||||
|
$data = $request->only(['changelog', 'file_name']);
|
||||||
|
|
||||||
|
if ($request->filled('file_name') && $request->input('file_name') !== $app->file_name) {
|
||||||
|
$newFileName = $request->input('file_name');
|
||||||
|
$newPath = 'apk/' . $newFileName;
|
||||||
|
|
||||||
|
if (Storage::disk('public')->exists($newPath)) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Ya existe un archivo con ese nombre.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::disk('public')->move($app->path, $newPath);
|
||||||
|
|
||||||
|
$data['file_name'] = $newFileName;
|
||||||
|
$data['path'] = $newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$app->update($data);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Versión actualizada correctamente',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar una versión
|
||||||
|
*/
|
||||||
|
public function destroy(ApkVersion $app)
|
||||||
|
{
|
||||||
|
Storage::disk('public')->delete($app->path);
|
||||||
|
$app->delete();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Versión eliminada correctamente',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descargar APK más reciente y registrar log
|
||||||
|
*/
|
||||||
|
public function download()
|
||||||
|
{
|
||||||
|
$version = ApkVersion::latest()->first();
|
||||||
|
|
||||||
|
if (!$version) {
|
||||||
|
abort(404, 'No hay APK disponible para descargar.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkLog::create([
|
||||||
|
'apk_version_id' => $version->id,
|
||||||
|
'downloaded_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect(asset('storage/' . $version->path));
|
||||||
|
}
|
||||||
|
}
|
||||||
395
app/Http/Controllers/Repuve/CancellationController.php
Normal file
395
app/Http/Controllers/Repuve/CancellationController.php
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Repuve\CancelConstanciaRequest;
|
||||||
|
use App\Models\CatalogCancellationReason;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use App\Models\Record;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use App\Models\TagCancellationLog;
|
||||||
|
use App\Models\VehicleTagLog;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
|
||||||
|
class CancellationController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Middleware
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('cancellations.cancel_constancia', ['cancelarConstancia']),
|
||||||
|
self::can('cancellations.cancel_tag_no_asignado', ['cancelarTagNoAsignado']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancelar la constancia/tag
|
||||||
|
*/
|
||||||
|
public function cancelarConstancia(CancelConstanciaRequest $request, $recordId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Buscar el expediente con sus relaciones
|
||||||
|
$record = Record::with('vehicle.tag.status')->find($recordId);
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'El expediente no existe.',
|
||||||
|
'record_id' => $recordId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$vehicle = $record->vehicle;
|
||||||
|
$tag = $vehicle->tag;
|
||||||
|
|
||||||
|
// Validar que el vehículo tiene un tag asignado
|
||||||
|
if (!$tag) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El vehículo no tiene un tag/constancia asignada.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que el tag está en estado activo O cancelado (para permitir sustitución posterior)
|
||||||
|
if (!$tag->isAssigned() && !$tag->isCancelled()) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El tag debe estar asignado o cancelado. Status actual: ' . $tag->status->name
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que se proporcionen los datos de sustitución
|
||||||
|
if (!$request->filled('new_folio') || !$request->filled('new_tag_number')) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Para cancelar la constancia, debe proporcionar: nuevo folio y numero de constancia.',
|
||||||
|
'provided' => [
|
||||||
|
'new_folio' => $request->filled('new_folio'),
|
||||||
|
'new_tag_number' => $request->filled('new_tag_number'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que el tag_number tenga exactamente 17 caracteres
|
||||||
|
if (strlen($request->new_tag_number) !== 32) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El tag_number debe tener exactamente 32 caracteres',
|
||||||
|
'provided_tag_number' => $request->new_tag_number,
|
||||||
|
'length' => strlen($request->new_tag_number),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isSubstitution = true;
|
||||||
|
|
||||||
|
// Guardar información del tag anterior ANTES de cancelarlo
|
||||||
|
$oldTagNumber = $tag->tag_number;
|
||||||
|
$oldFolio = $tag->folio;
|
||||||
|
|
||||||
|
// Crear registro en el log de vehículos
|
||||||
|
if ($tag->isAssigned()) {
|
||||||
|
$cancellationLog = VehicleTagLog::create([
|
||||||
|
'vehicle_id' => $vehicle->id,
|
||||||
|
'tag_id' => $tag->id,
|
||||||
|
'action_type' => 'cancelacion',
|
||||||
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||||
|
'cancellation_observations' => $request->cancellation_observations,
|
||||||
|
'cancellation_at' => now(),
|
||||||
|
'cancelled_by' => Auth::id(),
|
||||||
|
'performed_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Actualizar estado del tag a 'cancelled'
|
||||||
|
$tag->markAsCancelled();
|
||||||
|
} else {
|
||||||
|
// Si ya estaba cancelado, buscar el último log de cancelación
|
||||||
|
$cancellationLog = VehicleTagLog::where('vehicle_id', $vehicle->id)
|
||||||
|
->where('tag_id', $tag->id)
|
||||||
|
->where('action_type', 'cancelacion')
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
$newTag = null;
|
||||||
|
$substitutionLog = null;
|
||||||
|
|
||||||
|
if ($isSubstitution) {
|
||||||
|
// Buscar el nuevo tag por folio
|
||||||
|
$newTag = Tag::where('folio', $request->new_folio)->first();
|
||||||
|
|
||||||
|
if (!$newTag) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'El tag con el folio proporcionado no existe.',
|
||||||
|
'new_folio' => $request->new_folio,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$newTag->isAvailable()) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El nuevo tag no está disponible para asignación',
|
||||||
|
'new_folio' => $request->new_folio,
|
||||||
|
'current_status' => $newTag->status->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asignar tag_number al nuevo tag si no lo tiene
|
||||||
|
if (!$newTag->tag_number) {
|
||||||
|
// Validar que el tag_number no esté usado por otro tag
|
||||||
|
$existingTag = Tag::where('tag_number', $request->new_tag_number)
|
||||||
|
->where('id', '!=', $newTag->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingTag) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El tag_number ya está asignado a otro tag.',
|
||||||
|
'new_tag_number' => $request->new_tag_number,
|
||||||
|
'folio_existente' => $existingTag->folio,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newTag->tag_number = $request->new_tag_number;
|
||||||
|
$newTag->save();
|
||||||
|
} elseif ($newTag->tag_number !== $request->new_tag_number) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El tag ya tiene un tag_number diferente asignado.',
|
||||||
|
'assigned_tag_number' => $newTag->tag_number,
|
||||||
|
'provided_tag_number' => $request->new_tag_number,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desasignar el tag viejo para evitar conflicto de unique constraint
|
||||||
|
$tag->update(['vehicle_id' => null]);
|
||||||
|
|
||||||
|
// Asignar el nuevo tag al vehículo (usa el folio del tag encontrado)
|
||||||
|
$newTag->markAsAssigned($vehicle->id, $newTag->folio);
|
||||||
|
|
||||||
|
// Crear log de sustitución
|
||||||
|
$substitutionLog = VehicleTagLog::create([
|
||||||
|
'vehicle_id' => $vehicle->id,
|
||||||
|
'tag_id' => $newTag->id,
|
||||||
|
'action_type' => 'sustitucion',
|
||||||
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||||
|
'cancellation_observations' => 'Tag sustituido. Tag anterior: ' . $oldTagNumber . ' (Folio: ' . $oldFolio . '). Motivo: ' . ($request->cancellation_observations ?? ''),
|
||||||
|
'performed_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Actualizar el folio del expediente con el folio del nuevo tag
|
||||||
|
$record->update(['folio' => $newTag->folio]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$message = $isSubstitution
|
||||||
|
? 'Tag cancelado y sustituido exitosamente'
|
||||||
|
: 'Constancia cancelada exitosamente';
|
||||||
|
|
||||||
|
// Agregar alerta si NO hay sustitución
|
||||||
|
$alert = null;
|
||||||
|
if (!$isSubstitution) {
|
||||||
|
$alert = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => 'El tag ha sido cancelado y necesita sustitución',
|
||||||
|
'requires_action' => true,
|
||||||
|
'cancellation_date' => $cancellationLog->cancellation_at->format('d/m/Y H:i:s'),
|
||||||
|
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => $message,
|
||||||
|
'is_substitution' => $isSubstitution,
|
||||||
|
'alert' => $alert,
|
||||||
|
'cancellation' => [
|
||||||
|
'id' => $cancellationLog->id,
|
||||||
|
'vehicle' => [
|
||||||
|
'id' => $vehicle->id,
|
||||||
|
'placa' => $vehicle->placa,
|
||||||
|
'niv' => $vehicle->niv,
|
||||||
|
],
|
||||||
|
'old_tag' => [
|
||||||
|
'id' => $tag->id,
|
||||||
|
'folio' => $oldFolio,
|
||||||
|
'tag_number' => $oldTagNumber,
|
||||||
|
'new_status' => 'Cancelado',
|
||||||
|
],
|
||||||
|
'new_tag' => $newTag ? [
|
||||||
|
'id' => $newTag->id,
|
||||||
|
'folio' => $newTag->folio,
|
||||||
|
'tag_number' => $newTag->tag_number,
|
||||||
|
'status' => $newTag->status->name,
|
||||||
|
] : null,
|
||||||
|
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
||||||
|
'cancellation_observations' => $request->cancellation_observations,
|
||||||
|
'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(),
|
||||||
|
'cancelled_by' => Auth::user()->name,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('Error en cancelarConstancia: ' . $e->getMessage(), [
|
||||||
|
'record_id' => $recordId ?? null,
|
||||||
|
'cancellation_reason' => $request->cancellation_reason ?? null,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Error al cancelar la constancia',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelarTagNoAsignado(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'folio' => 'required|string|exists:tags,folio',
|
||||||
|
'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id',
|
||||||
|
'cancellation_observations' => 'nullable|string',
|
||||||
|
'module_id' => 'nullable|exists:modules,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$tag = Tag::where('folio', $request->folio)->first();
|
||||||
|
|
||||||
|
if (!$tag) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'No se encontró el tag con el folio proporcionado.',
|
||||||
|
'folio' => $request->folio,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que el tag NO esté asignado
|
||||||
|
if ($tag->isAssigned()) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Este tag está asignado a un vehículo. Usa el endpoint de cancelación de constancia.',
|
||||||
|
'tag_status' => $tag->status->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que no esté ya cancelado
|
||||||
|
if ($tag->isCancelled()) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El tag ya está cancelado.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$observations = $request->cancellation_observations;
|
||||||
|
|
||||||
|
// Verificar que existe el motivo de cancelación ANTES de crear el log
|
||||||
|
$cancellationReason = CatalogCancellationReason::find($request->cancellation_reason_id);
|
||||||
|
if (!$cancellationReason) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'El motivo de cancelación no existe.',
|
||||||
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cancellationLog = TagCancellationLog::create([
|
||||||
|
'tag_id' => $tag->id,
|
||||||
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
||||||
|
'cancellation_observations' => $observations,
|
||||||
|
'cancellation_at' => now(),
|
||||||
|
'cancelled_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cargar las relaciones necesarias ANTES de usarlas
|
||||||
|
$cancellationLog->load(['cancellationReason', 'cancelledBy']);
|
||||||
|
|
||||||
|
// Actualizar el módulo del tag: usar el enviado o el del usuario autenticado
|
||||||
|
$moduleId = $request->filled('module_id') ? $request->module_id : Auth::user()->module_id;
|
||||||
|
if ($moduleId) {
|
||||||
|
$tag->module_id = $moduleId;
|
||||||
|
$tag->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancelar el tag
|
||||||
|
$tag->markAsDamaged();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
// Recargar el tag con sus relaciones actualizadas
|
||||||
|
$tag->load(['status', 'cancellationLogs.cancellationReason', 'cancellationLogs.cancelledBy', 'module']);
|
||||||
|
|
||||||
|
// Obtener datos de cancelación del último log
|
||||||
|
$lastCancellation = $tag->cancellationLogs()
|
||||||
|
->with(['cancellationReason', 'cancelledBy'])
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Validar que existe el log de cancelación
|
||||||
|
if (!$lastCancellation) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error: No se encontró el log de cancelación después de crearlo.',
|
||||||
|
'tag_id' => $tag->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar datos para el PDF con validaciones defensivas
|
||||||
|
$cancellationData = [
|
||||||
|
'fecha' => $lastCancellation->cancellation_at ? $lastCancellation->cancellation_at->format('d/m/Y') : now()->format('d/m/Y'),
|
||||||
|
'folio' => $tag->folio ?? '',
|
||||||
|
'tag_number' => $tag->tag_number ?? 'N/A',
|
||||||
|
'motivo' => ($lastCancellation->cancellationReason && isset($lastCancellation->cancellationReason->name))
|
||||||
|
? $lastCancellation->cancellationReason->name
|
||||||
|
: 'No especificado',
|
||||||
|
'operador' => ($lastCancellation->cancelledBy && isset($lastCancellation->cancelledBy->full_name))
|
||||||
|
? $lastCancellation->cancelledBy->full_name
|
||||||
|
: 'Sistema',
|
||||||
|
'modulo' => ($tag->module && isset($tag->module->name)) ? $tag->module->name : 'No especificado',
|
||||||
|
'ubicacion' => ($tag->module && isset($tag->module->address)) ? $tag->module->address : 'No especificado',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdf = Pdf::loadView('pdfs.tag', [
|
||||||
|
'cancellation' => $cancellationData,
|
||||||
|
])
|
||||||
|
->setPaper('a4', 'portrait')
|
||||||
|
->setOptions([
|
||||||
|
'defaultFont' => 'sans-serif',
|
||||||
|
'isHtml5ParserEnabled' => true,
|
||||||
|
'isRemoteEnabled' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $pdf->stream('constancia_cancelada_' . ($tag->tag_number ?? $tag->folio) . '.pdf');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Retornar error como JSON
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Tag cancelado pero error al generar PDF',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
// Errores de validación
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Error de validación',
|
||||||
|
'errors' => $e->errors(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al cancelar el tag',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/Http/Controllers/Repuve/CatalogController.php
Normal file
108
app/Http/Controllers/Repuve/CatalogController.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Repuve\CatalogCancellationReasonStoreRequest;
|
||||||
|
use App\Http\Requests\Repuve\CatalogCancellationReasonUpdateRequest;
|
||||||
|
use App\Models\CatalogCancellationReason;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*/
|
||||||
|
class CatalogController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$type = $request->query('type');
|
||||||
|
|
||||||
|
$query = CatalogCancellationReason::query();
|
||||||
|
|
||||||
|
if ($type === 'cancelacion') {
|
||||||
|
$query->forCancellation();
|
||||||
|
} elseif ($type === 'sustitucion') {
|
||||||
|
$query->forSubstitution();
|
||||||
|
} else {
|
||||||
|
$query->orderBy('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reasons = $query->get(['id', 'code', 'name', 'description', 'applies_to']);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Razones de cancelación obtenidas exitosamente',
|
||||||
|
'data' => $reasons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$reason = CatalogCancellationReason::find($id);
|
||||||
|
|
||||||
|
if (!$reason) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Razón de cancelación no encontrada',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Razón de cancelación obtenida exitosamente',
|
||||||
|
'data' => $reason,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(CatalogCancellationReasonStoreRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$reason = CatalogCancellationReason::create($validated);
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'message' => 'Razón de cancelación creada exitosamente',
|
||||||
|
'data' => $reason,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(CatalogCancellationReasonUpdateRequest $request, $id)
|
||||||
|
{
|
||||||
|
$reason = CatalogCancellationReason::find($id);
|
||||||
|
|
||||||
|
if (!$reason) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Razón de cancelación no encontrada',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$reason->update($validated);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Razón de cancelación actualizada exitosamente',
|
||||||
|
'data' => $reason,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$reason = CatalogCancellationReason::find($id);
|
||||||
|
|
||||||
|
if (!$reason) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Razón de cancelación no encontrada',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reason->delete();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Razón de cancelación eliminada exitosamente',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/Http/Controllers/Repuve/CatalogNameImgController.php
Normal file
76
app/Http/Controllers/Repuve/CatalogNameImgController.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use App\Http\Requests\Repuve\CatalogNameImgStoreRequest;
|
||||||
|
use App\Http\Requests\Repuve\CatalogNameImgUpdateRequest;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\CatalogNameImg;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
class CatalogNameImgController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Listar
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$names = CatalogNameImg::orderBy('id', 'ASC')->get();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'names' => $names,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear
|
||||||
|
*/
|
||||||
|
public function store(CatalogNameImgStoreRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$catalogNameImg = CatalogNameImg::create($validated);
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'name' => $catalogNameImg,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar
|
||||||
|
*/
|
||||||
|
public function update(CatalogNameImgUpdateRequest $request, $id)
|
||||||
|
{
|
||||||
|
$catalogName = CatalogNameImg::findOrFail($id);
|
||||||
|
|
||||||
|
$validated = $request->validated([
|
||||||
|
'name' => 'required|string|max:255|unique:catalog_name_img,name,' . $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$catalogName->update($validated);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'name' => $catalogName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar
|
||||||
|
*/
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$catalogName = CatalogNameImg::findOrFail($id);
|
||||||
|
$catalogName->delete();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Nombre del catálogo eliminado correctamente.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al eliminar el nombre del catálogo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
app/Http/Controllers/Repuve/DeviceController.php
Normal file
297
app/Http/Controllers/Repuve/DeviceController.php
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
use App\Http\Requests\Repuve\DeviceStoreRequest;
|
||||||
|
use App\Http\Requests\Repuve\DeviceUpdateRequest;
|
||||||
|
use App\Models\Device;
|
||||||
|
use App\Models\DeviceModule;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
|
||||||
|
class DeviceController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('devices.index', ['index']),
|
||||||
|
self::can('devices.show', ['show']),
|
||||||
|
self::can('devices.destroy', ['destroy']),
|
||||||
|
self::can('devices.toggle_status', ['toggleStatus']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$devices = Device::query()->with('deviceModules.module', 'deviceModules.user');
|
||||||
|
|
||||||
|
if ($request->filled('serie')) {
|
||||||
|
$devices->where('serie', 'LIKE', '%' . $request->input('serie') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('brand')) {
|
||||||
|
$devices->where('brand', 'LIKE', '%' . $request->input('brand') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'devices' => $devices->paginate(config('app.pagination')),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al obtener la lista de dispositivos.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(DeviceStoreRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Crear el dispositivo
|
||||||
|
$device = Device::create([
|
||||||
|
'brand' => $request->input('brand'),
|
||||||
|
'serie' => $request->input('serie'),
|
||||||
|
'mac_address' => $request->input('mac_address'),
|
||||||
|
'status' => $request->input('status', true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Asignar módulo y usuarios usando el modelo DeviceModule
|
||||||
|
$userIds = $request->input('user_id', []);
|
||||||
|
|
||||||
|
if (!empty($userIds)) {
|
||||||
|
// Si hay usuarios, crear un registro por cada usuario
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
DeviceModule::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'module_id' => $request->module_id,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'status' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si no hay usuarios, crear solo la relación device-module
|
||||||
|
DeviceModule::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'module_id' => $request->module_id,
|
||||||
|
'user_id' => null,
|
||||||
|
'status' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$device->load('deviceModules.module');
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'message' => 'Dispositivo creado exitosamente.',
|
||||||
|
'device' => [
|
||||||
|
'id' => $device->id,
|
||||||
|
'brand' => $device->brand,
|
||||||
|
'serie' => $device->serie,
|
||||||
|
'status' => $device->status,
|
||||||
|
'module' => $device->deviceModules->first()?->module,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al crear el dispositivo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$device = Device::with('deviceModules.module', 'deviceModules.user')->findOrFail($id);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'device' => $device,
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Dispositivo no encontrado.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al obtener el dispositivo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(DeviceUpdateRequest $request, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$device = Device::findOrFail($id);
|
||||||
|
|
||||||
|
// Validar unicidad solo si los valores cambiaron
|
||||||
|
if ($request->filled('serie') && $request->serie !== $device->serie) {
|
||||||
|
if (Device::where('serie', $request->serie)->exists()) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
|
'serie' => ['El número de serie ya está en uso.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('mac_address') && $request->mac_address !== $device->mac_address) {
|
||||||
|
if (Device::where('mac_address', $request->mac_address)->exists()) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
|
'mac_address' => ['La dirección MAC ya está registrada.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$device->update($request->only(['brand', 'serie', 'mac_address', 'status']));
|
||||||
|
|
||||||
|
DeviceModule::where('device_id', $device->id)->delete();
|
||||||
|
|
||||||
|
$userIds = $request->input('user_id', []);
|
||||||
|
|
||||||
|
if (!empty($userIds)) {
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
DeviceModule::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'module_id' => $request->module_id,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'status' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DeviceModule::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'module_id' => $request->module_id,
|
||||||
|
'user_id' => null,
|
||||||
|
'status' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$device->load(['deviceModules.module', 'deviceModules.user']);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Dispositivo actualizado exitosamente.',
|
||||||
|
'device' => [
|
||||||
|
'id' => $device->id,
|
||||||
|
'brand' => $device->brand,
|
||||||
|
'serie' => $device->serie,
|
||||||
|
'mac_address' => $device->mac_address,
|
||||||
|
'status' => $device->status,
|
||||||
|
'module' => $device->deviceModules->first()?->module,
|
||||||
|
'authorized_users' => $device->deviceModules
|
||||||
|
->filter(fn($dm) => $dm->user !== null)
|
||||||
|
->map(fn($dm) => [
|
||||||
|
'id' => $dm->user->id,
|
||||||
|
'name' => $dm->user->full_name,
|
||||||
|
'username' => $dm->user->username,
|
||||||
|
])
|
||||||
|
->unique('id')
|
||||||
|
->values(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Dispositivo no encontrado.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al actualizar el dispositivo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$device = Device::findOrFail($id);
|
||||||
|
$device->delete();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Dispositivo eliminado exitosamente.',
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Dispositivo no encontrado.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al eliminar el dispositivo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cambiar solo el status de un dispositivo
|
||||||
|
*/
|
||||||
|
public function toggleStatus(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$device = Device::findOrFail($id);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$newStatus = !$device->status;
|
||||||
|
$device->update([
|
||||||
|
'status' => $newStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$device->refresh();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => $device->status
|
||||||
|
? 'Dispositivo activado exitosamente'
|
||||||
|
: 'Dispositivo desactivado exitosamente',
|
||||||
|
'device' => [
|
||||||
|
'id' => $device->id,
|
||||||
|
'brand' => $device->brand,
|
||||||
|
'serie' => $device->serie,
|
||||||
|
'mac_address' => $device->mac_address,
|
||||||
|
'status' => $device->status ? 'Activo' : 'Inactivo',
|
||||||
|
'updated_at' => $device->updated_at->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Dispositivo no encontrado',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error al cambiar status del módulo: ' . $e->getMessage(), [
|
||||||
|
'module_id' => $id,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al cambiar status del módulo',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1526
app/Http/Controllers/Repuve/ExcelController.php
Normal file
1526
app/Http/Controllers/Repuve/ExcelController.php
Normal file
File diff suppressed because it is too large
Load Diff
621
app/Http/Controllers/Repuve/InscriptionController.php
Normal file
621
app/Http/Controllers/Repuve/InscriptionController.php
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
<?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 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 para corroborar el vehículo y obtener folio_CI
|
||||||
|
$repuveNacionalData = $this->repuveService->consultarVehiculo($niv, $placa);
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
// Verificar robo
|
||||||
|
$roboResult = $this->checkIfStolen($niv, $placa);
|
||||||
|
// 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,
|
||||||
|
'performed_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Crear registro
|
||||||
|
$record = Record::create([
|
||||||
|
'folio' => $folio,
|
||||||
|
'vehicle_id' => $vehicle->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'module_id' => Auth::user()->module_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Procesar archivos
|
||||||
|
$uploadedFiles = [];
|
||||||
|
if ($request->hasFile('files')) {
|
||||||
|
$files = $request->file('files');
|
||||||
|
$nameIds = $request->input('name_id', []);
|
||||||
|
|
||||||
|
if (!empty($nameIds)) {
|
||||||
|
$validIds = CatalogNameImg::whereIn('id', $nameIds)->pluck('id')->toArray();
|
||||||
|
if (count($validIds) !== count($nameIds)) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Algunos IDs del catálogo de nombres no son válidos',
|
||||||
|
'provided_id' => $nameIds,
|
||||||
|
'valid_id' => $validIds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($files as $index => $file) {
|
||||||
|
// Obtener el name_id del request o usar null como fallback
|
||||||
|
$nameId = $nameIds[$index] ?? null;
|
||||||
|
|
||||||
|
if ($nameId === null) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => "Falta el name_id para el archivo en el índice {$index}",
|
||||||
|
'file_index' => $index,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el nombre del catálogo para el nombre del archivo
|
||||||
|
$catalogName = CatalogNameImg::find($nameId);
|
||||||
|
$extension = $file->getClientOriginalExtension();
|
||||||
|
$fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension;
|
||||||
|
$path = $file->storeAs("records/{$record->folio}", $fileName, 'public');
|
||||||
|
$md5 = md5_file($file->getRealPath());
|
||||||
|
|
||||||
|
$fileRecord = File::create([
|
||||||
|
'name_id' => $nameId,
|
||||||
|
'path' => $path,
|
||||||
|
'md5' => $md5,
|
||||||
|
'record_id' => $record->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$uploadedFiles[] = [
|
||||||
|
'id' => $fileRecord->id,
|
||||||
|
'name' => $catalogName->name,
|
||||||
|
'path' => $fileRecord->path,
|
||||||
|
'url' => $fileRecord->url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Http/Controllers/Repuve/LogsController.php
Normal file
54
app/Http/Controllers/Repuve/LogsController.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\LogsService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*/
|
||||||
|
class LogsController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
public function __construct(private LogsService $logsService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function repuveLogs(Request $request)
|
||||||
|
{
|
||||||
|
$filters = $this->filters($request);
|
||||||
|
$logs = $this->logsService->readRepuve($filters);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'source' => 'repuve',
|
||||||
|
'filters' => $filters,
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function padronEstatalLogs(Request $request)
|
||||||
|
{
|
||||||
|
$filters = $this->filters($request);
|
||||||
|
$logs = $this->logsService->readPadronEstatal($filters);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'source' => 'padron-estatal',
|
||||||
|
'filters' => $filters,
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filters(Request $request): array
|
||||||
|
{
|
||||||
|
return $request->validate([
|
||||||
|
'start_date' => ['nullable', 'date'],
|
||||||
|
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
269
app/Http/Controllers/Repuve/ModuleController.php
Normal file
269
app/Http/Controllers/Repuve/ModuleController.php
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Repuve;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Repuve\ModuleStoreRequest;
|
||||||
|
use App\Http\Requests\Repuve\ModuleUpdateRequest;
|
||||||
|
use App\Models\Module;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
|
||||||
|
class ModuleController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Middleware
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('modules.index', ['index']),
|
||||||
|
self::can('modules.show', ['show']),
|
||||||
|
self::can('modules.destroy', ['destroy']),
|
||||||
|
self::can('modules.toggle_status', ['toggleStatus']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listar módulos existentes
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$modules = Module::with([
|
||||||
|
'responsible:id,name,username',
|
||||||
|
'municipality:id,code,name',
|
||||||
|
'users:id,name,paternal,maternal,username,module_id',
|
||||||
|
'users.roles:id,name,description'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filtro por nombre
|
||||||
|
if ($request->filled('name')) {
|
||||||
|
$modules->where('name', 'like', '%' . $request->input('name') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('municipality')) {
|
||||||
|
$modules->whereHas('municipality', function ($q) use ($request) {
|
||||||
|
$q->where('name', 'like', '%' . $request->input('municipality') . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'modules' => $modules->paginate(config('app.pagination')),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al listar módulos',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear un nuevo módulo
|
||||||
|
*/
|
||||||
|
public function store(ModuleStoreRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Crear el módulo
|
||||||
|
$module = Module::create([
|
||||||
|
'name' => $request->input('name'),
|
||||||
|
'responsible_id' => $request->input('responsible_id'),
|
||||||
|
'municipality_id' => $request->input('municipality_id'),
|
||||||
|
'address' => $request->input('address'),
|
||||||
|
'longitude' => $request->input('longitude'),
|
||||||
|
'latitude' => $request->input('latitude'),
|
||||||
|
'status' => $request->input('status', true), // Por defecto activo
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$module->load('municipality');
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'message' => 'Módulo creado exitosamente',
|
||||||
|
'module' => [
|
||||||
|
'name' => $module->name,
|
||||||
|
'responsible_id' => $module->responsible_id,
|
||||||
|
'municipality' => $module->municipality ? [
|
||||||
|
'id' => $module->municipality->id,
|
||||||
|
'code' => $module->municipality->code,
|
||||||
|
'name' => $module->municipality->name,
|
||||||
|
] : null,
|
||||||
|
'address' => $module->address,
|
||||||
|
'longitude' => $module->longitude,
|
||||||
|
'latitude' => $module->latitude,
|
||||||
|
'status' => $module->status ? 'Activo' : 'Inactivo',
|
||||||
|
'created_at' => $module->created_at->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al crear módulo',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$module = Module::with([
|
||||||
|
'responsible:id,name,username',
|
||||||
|
'municipality:id,code,name',
|
||||||
|
'users:id,name,paternal,maternal,username,module_id',
|
||||||
|
'users.roles:id,name,description'
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'module' => $module,
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Módulo no encontrado.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al obtener el módulo.',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar un módulo existente
|
||||||
|
*/
|
||||||
|
public function update(ModuleUpdateRequest $request, int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$module = Module::findOrFail($id);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Actualizar solo los campos que vienen en el request
|
||||||
|
$module->update($request->validated());
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
// Cargar la relación actualizada
|
||||||
|
$module->load('municipality');
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Módulo actualizado exitosamente',
|
||||||
|
'module' => [
|
||||||
|
'name' => $module->name,
|
||||||
|
'responsible_id' => $module->responsible_id,
|
||||||
|
'municipality' => $module->municipality ? [
|
||||||
|
'id' => $module->municipality->id,
|
||||||
|
'code' => $module->municipality->code,
|
||||||
|
'name' => $module->municipality->name,
|
||||||
|
] : null,
|
||||||
|
'address' => $module->address,
|
||||||
|
'longitude' => $module->longitude,
|
||||||
|
'latitude' => $module->latitude,
|
||||||
|
'status' => $module->status ? 'Activo' : 'Inactivo',
|
||||||
|
'updated_at' => $module->updated_at->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Módulo no encontrado',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al actualizar módulo',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$module = Module::findOrFail($id);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$module->delete();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Módulo eliminado exitosamente',
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Módulo no encontrado',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error al eliminar módulo: ' . $e->getMessage(), [
|
||||||
|
'module_id' => $id,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al eliminar el módulo',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cambiar solo el status de un módulo
|
||||||
|
*/
|
||||||
|
public function toggleStatus(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$module = Module::findOrFail($id);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$newStatus = !$module->status;
|
||||||
|
|
||||||
|
$module->update([
|
||||||
|
'status' => $newStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$module->refresh();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => $module->status
|
||||||
|
? 'Módulo activado exitosamente'
|
||||||
|
: 'Módulo desactivado exitosamente',
|
||||||
|
'module' => [
|
||||||
|
'id' => $module->id,
|
||||||
|
'name' => $module->name,
|
||||||
|
'status' => $module->status,
|
||||||
|
'status_text' => $module->status ? 'Activo' : 'Inactivo',
|
||||||
|
'updated_at' => $module->updated_at->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Módulo no encontrado',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
Log::error('Error al cambiar status del módulo: ' . $e->getMessage(), [
|
||||||
|
'module_id' => $id,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Error al cambiar status del módulo',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Http/Controllers/Repuve/MunicipalityController.php
Normal file
72
app/Http/Controllers/Repuve/MunicipalityController.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php namespace App\Http\Controllers\Repuve;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Models\Municipality;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class MunicipalityController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$municipalities = Municipality::orderBy('id', 'ASC')->get();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'data' => $municipalities,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'code' => 'required|unique:municipalities,code',
|
||||||
|
'name' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$municipality = Municipality::create([
|
||||||
|
'code' => $request->input('code'),
|
||||||
|
'name' => $request->input('name'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'data' => $municipality,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$municipality = Municipality::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'code' => 'required|unique:municipalities,code,' . $municipality->id,
|
||||||
|
'name' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$municipality->update([
|
||||||
|
'code' => $request->input('code'),
|
||||||
|
'name' => $request->input('name'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'data' => $municipality,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$municipality = Municipality::findOrFail($id);
|
||||||
|
$municipality->delete();
|
||||||
|
|
||||||
|
return ApiResponse::NO_CONTENT->response();
|
||||||
|
}
|
||||||
|
}
|
||||||
470
app/Http/Controllers/Repuve/PackageController.php
Normal file
470
app/Http/Controllers/Repuve/PackageController.php
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
<?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 en el mismo lote antes de crear
|
||||||
|
$packageIds = Package::where('lot', $request->lot)->pluck('id');
|
||||||
|
|
||||||
|
$conflicting = Tag::whereIn('package_id', $packageIds)
|
||||||
|
->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([
|
||||||
|
'starting_page' => [
|
||||||
|
'Los folios ' . $conflicting->join(', ') . ' ya están registrados en el lote "' . $request->lot . '".',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $request->validated();
|
||||||
|
$form['user_id'] = Auth::id();
|
||||||
|
|
||||||
|
$package = Package::create($form);
|
||||||
|
|
||||||
|
$statusAvailable = CatalogTagStatus::where('code', 'available')->firstOrFail();
|
||||||
|
|
||||||
|
for ($page = $request->starting_page; $page <= $request->ending_page; $page++) {
|
||||||
|
Tag::create([
|
||||||
|
'folio' => $page,
|
||||||
|
'tag_number' => null,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'status_id' => $statusAvailable->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'message' => 'Paquete registrado exitosamente con sus tags',
|
||||||
|
'package' => $package->load('tags'),
|
||||||
|
'tags_created' => $package->tags()->count(),
|
||||||
|
]);
|
||||||
|
} catch (\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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
693
app/Http/Controllers/Repuve/RecordController.php
Normal file
693
app/Http/Controllers/Repuve/RecordController.php
Normal file
@ -0,0 +1,693 @@
|
|||||||
|
<?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['id_chip'] = $oldTagLog->vehicle->id_chip ?? '';
|
||||||
|
$data['placa'] = $oldTagLog->vehicle->placa ?? '';
|
||||||
|
$data['niv'] = $oldTagLog->vehicle->niv ?? '';
|
||||||
|
|
||||||
|
// tag NUEVO
|
||||||
|
$newTag = $oldTagLog->vehicle->tag;
|
||||||
|
$data['folio_sustituto'] = $newTag?->folio ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargar módulo del usuario
|
||||||
|
*/
|
||||||
|
private function loadUserModule($user, &$data)
|
||||||
|
{
|
||||||
|
// Intentar cargar module
|
||||||
|
$user->load('module');
|
||||||
|
|
||||||
|
// Si no tiene module, usar responsibleModule
|
||||||
|
if (!$user->module) {
|
||||||
|
$user->load('responsibleModule');
|
||||||
|
|
||||||
|
if ($user->responsibleModule) {
|
||||||
|
$data['modulo'] = $user->responsibleModule->name;
|
||||||
|
$data['ubicacion'] = $user->responsibleModule->address;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$data['modulo'] = $user->module->name;
|
||||||
|
$data['ubicacion'] = $user->module->address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function errors(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'folio' => 'nullable|string',
|
||||||
|
'placa' => 'nullable|string',
|
||||||
|
'vin' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$records = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'user', 'error'])
|
||||||
|
->whereNotNull('api_response')
|
||||||
|
->whereRaw("JSON_EXTRACT(api_response, '$.has_error') = true")
|
||||||
|
->orderBy('id', 'ASC');
|
||||||
|
|
||||||
|
if ($request->filled('folio')) {
|
||||||
|
$records->where('folio', 'LIKE', '%' . $request->input('folio') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('placa')) {
|
||||||
|
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||||
|
$q->where('placa', 'LIKE', '%' . $request->input('placa') . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('vin')) {
|
||||||
|
$records->whereHas('vehicle', function ($q) use ($request) {
|
||||||
|
$q->where('niv', 'LIKE', '%' . $request->input('vin') . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Expedientes con errores encontrados exitosamente',
|
||||||
|
'records' => $records->paginate(config('app.pagination')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
585
app/Http/Controllers/Repuve/TagsController.php
Normal file
585
app/Http/Controllers/Repuve/TagsController.php
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
<?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'];
|
||||||
|
|
||||||
|
// Verificar si el folio está fuera del rango actual del paquete
|
||||||
|
$packageUpdated = false;
|
||||||
|
$rangeChanges = [];
|
||||||
|
$missingTags = [];
|
||||||
|
|
||||||
|
// Caso 1: El folio es MENOR que el starting_page (crear tags intermedios)
|
||||||
|
if ($folioNumerico < $package->starting_page) {
|
||||||
|
$rangeChanges['starting_page'] = [
|
||||||
|
'old' => $package->starting_page,
|
||||||
|
'new' => $folioNumerico,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Crear tags intermedios (desde el nuevo folio hasta el starting_page - 1)
|
||||||
|
for ($i = $folioNumerico + 1; $i < $package->starting_page; $i++) {
|
||||||
|
// Verificar que el tag no exista
|
||||||
|
$existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first();
|
||||||
|
if (!$existingTag) {
|
||||||
|
Tag::create([
|
||||||
|
'folio' => $i,
|
||||||
|
'tag_number' => null,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'module_id' => null,
|
||||||
|
'status_id' => $statusAvailable->id,
|
||||||
|
'vehicle_id' => null,
|
||||||
|
]);
|
||||||
|
$missingTags[] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$package->starting_page = $folioNumerico;
|
||||||
|
$packageUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios)
|
||||||
|
if ($folioNumerico > $package->ending_page) {
|
||||||
|
$rangeChanges['ending_page'] = [
|
||||||
|
'old' => $package->ending_page,
|
||||||
|
'new' => $folioNumerico,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Crear tags intermedios (desde ending_page + 1 hasta el nuevo folio - 1)
|
||||||
|
for ($i = $package->ending_page + 1; $i < $folioNumerico; $i++) {
|
||||||
|
// Verificar que el tag no exista
|
||||||
|
$existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first();
|
||||||
|
if (!$existingTag) {
|
||||||
|
Tag::create([
|
||||||
|
'folio' => $i,
|
||||||
|
'tag_number' => null,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'module_id' => null,
|
||||||
|
'status_id' => $statusAvailable->id,
|
||||||
|
'vehicle_id' => null,
|
||||||
|
]);
|
||||||
|
$missingTags[] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$package->ending_page = $folioNumerico;
|
||||||
|
$packageUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar cambios en el paquete si es necesario
|
||||||
|
if ($packageUpdated) {
|
||||||
|
$package->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear el tag principal solicitado
|
||||||
|
$tag = Tag::create([
|
||||||
|
'folio' => $validated['folio'],
|
||||||
|
'tag_number' => $validated['tag_number'] ?? null,
|
||||||
|
'package_id' => $validated['package_id'],
|
||||||
|
'module_id' => $validated['module_id'] ?? null,
|
||||||
|
'status_id' => $statusAvailable->id,
|
||||||
|
'vehicle_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
// Cargar relaciones
|
||||||
|
$tag->load(['package', 'module', 'status']);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'message' => 'Tag creado correctamente.',
|
||||||
|
'tag' => $tag,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Agregar información de actualización del paquete si hubo cambios
|
||||||
|
if ($packageUpdated) {
|
||||||
|
$response['package_updated'] = true;
|
||||||
|
$response['package_range_changes'] = $rangeChanges;
|
||||||
|
$response['package_current_range'] = [
|
||||||
|
'starting_page' => $package->starting_page,
|
||||||
|
'ending_page' => $package->ending_page,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar información de tags intermedios creados
|
||||||
|
if (!empty($missingTags)) {
|
||||||
|
$response['missing_tags_created'] = $missingTags;
|
||||||
|
$response['missing_tags_count'] = count($missingTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response($response);
|
||||||
|
} catch (\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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1470
app/Http/Controllers/Repuve/UpdateController.php
Normal file
1470
app/Http/Controllers/Repuve/UpdateController.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlador de sesiones
|
* Controlador de sesiones
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class LoginController extends Controller
|
class LoginController extends Controller
|
||||||
@ -28,11 +28,11 @@ class LoginController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function login(LoginRequest $request)
|
public function login(LoginRequest $request)
|
||||||
{
|
{
|
||||||
$user = User::where('email', $request->get('email'))->first();
|
$user = User::where('username', $request->get('username'))->first();
|
||||||
|
|
||||||
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
|
||||||
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
'email' => ['Usuario no valido']
|
'username' => ['Credenciales inválidas']
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,27 +56,35 @@ public function logout()
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Contraseña olvidada
|
* Contraseña olvidada
|
||||||
|
* Nota: Sin email, el reset se maneja por token directo
|
||||||
*/
|
*/
|
||||||
public function forgotPassword(ForgotRequest $request)
|
public function forgotPassword(ForgotRequest $request)
|
||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
|
|
||||||
$user = User::where('email', $data['email'])->first();
|
$user = User::where('username', $data['username'])->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'username' => ['Usuario no encontrado']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$token = $this->generateToken($user);
|
$token = $this->generateToken($user);
|
||||||
|
|
||||||
$user->notify(new ForgotPasswordNotification($token));
|
// Sin email, retornar el token directamente (para uso administrativo)
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'is_sent' => true
|
'is_generated' => true,
|
||||||
|
'token' => $token,
|
||||||
|
'message' => 'Token generado. Válido por 15 minutos.',
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
Log::channel('mail')->info("Email: {$data['email']}");
|
Log::channel('mail')->info("Username: {$data['username']}");
|
||||||
Log::channel('mail')->error($th->getMessage());
|
Log::channel('mail')->error($th->getMessage());
|
||||||
|
|
||||||
return ApiResponse::INTERNAL_ERROR->response([
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
'is_sent' => false,
|
'is_generated' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +95,7 @@ public function forgotPassword(ForgotRequest $request)
|
|||||||
public function resetPassword(ResetPasswordRequest $request)
|
public function resetPassword(ResetPasswordRequest $request)
|
||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
|
|
||||||
$model = ResetPassword::with('user')->where('token', $data['token'])->first();
|
$model = ResetPassword::with('user')->where('token', $data['token'])->first();
|
||||||
|
|
||||||
if(!$model){
|
if(!$model){
|
||||||
@ -142,4 +150,4 @@ private function deleteToken($token)
|
|||||||
{
|
{
|
||||||
ResetPassword::where('token', $token)->delete();
|
ResetPassword::where('token', $token)->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
app/Http/Controllers/System/SettingsController.php
Normal file
120
app/Http/Controllers/System/SettingsController.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php namespace App\Http\Controllers\System;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Helpers\EncryptionHelper;
|
||||||
|
use App\Enums\SettingTypeEk;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*/
|
||||||
|
class SettingsController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::can('system.settings', ['show', 'update']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show()
|
||||||
|
{
|
||||||
|
$encryptedCredentials = Setting::value('repuve_federal_credentials');
|
||||||
|
|
||||||
|
if (!$encryptedCredentials) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'username' => '',
|
||||||
|
'password_exists' => false
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = EncryptionHelper::decryptData($encryptedCredentials);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'username' => $credentials['username'] ?? '',
|
||||||
|
'password' => $credentials['password'] ?? '',
|
||||||
|
'password_exists' => !empty($credentials['password'])
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decrypt(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'value' => 'required|string',
|
||||||
|
'app_key' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->filled('app_key')) {
|
||||||
|
try {
|
||||||
|
$rawKey = base64_decode(str_replace('base64:', '', $request->app_key));
|
||||||
|
$encrypter = new \Illuminate\Encryption\Encrypter($rawKey, 'AES-256-CBC');
|
||||||
|
$credentials = json_decode($encrypter->decryptString($request->value), true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'No se pudo desencriptar con el APP_KEY proporcionado',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$credentials = EncryptionHelper::decryptData($request->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$credentials) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'No se pudo desencriptar el valor proporcionado',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $credentials,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'required|string|min:6|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Preparar datos para encriptar
|
||||||
|
$credentials = [
|
||||||
|
'username' => $validated['username'],
|
||||||
|
'password' => $validated['password']
|
||||||
|
];
|
||||||
|
|
||||||
|
// Encriptar las credenciales
|
||||||
|
$encryptedValue = EncryptionHelper::encryptData($credentials);
|
||||||
|
|
||||||
|
// Guardar en BD (crea o actualiza automáticamente)
|
||||||
|
Setting::value(
|
||||||
|
key: 'repuve_federal_credentials',
|
||||||
|
value: $encryptedValue,
|
||||||
|
description: 'Credenciales encriptadas para REPUVE Federal',
|
||||||
|
type_ek: SettingTypeEk::JSON
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Credenciales guardadas correctamente',
|
||||||
|
'data' => [
|
||||||
|
'username' => $credentials['username'],
|
||||||
|
'password_exists' => true
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Solicitud de login
|
* Solicitud de login
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class LoginRequest extends FormRequest
|
class LoginRequest extends FormRequest
|
||||||
@ -30,8 +30,17 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'email'],
|
'username' => ['required', 'string'],
|
||||||
'password' => ['required', 'min:8'],
|
'password' => ['required', 'min:8'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username.required' => 'El usuario es requerido',
|
||||||
|
'password.required' => 'La contraseña es requerida',
|
||||||
|
'password.min' => 'La contraseña debe tener al menos 8 caracteres',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
app/Http/Requests/Repuve/ApkStorageRequest.php
Normal file
29
app/Http/Requests/Repuve/ApkStorageRequest.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ApkStorageRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return auth()->user()->HasPermissionTo('apk.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'apk' => 'required|file|max:153600',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'apk.required' => 'El archivo APK es obligatorio',
|
||||||
|
'apk.file' => 'El archivo debe ser un archivo válido',
|
||||||
|
'apk.max' => 'El archivo no debe superar los 150MB',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/Repuve/ApkUpdateRequest.php
Normal file
31
app/Http/Requests/Repuve/ApkUpdateRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ApkUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return auth()->user()->HasPermissionTo('apk.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'file_name' => 'nullable|string|max:255',
|
||||||
|
'changelog' => 'nullable|string|max:255',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'file_name.string' => 'El nombre del archivo debe ser una cadena de texto',
|
||||||
|
'file_name.max' => 'El nombre del archivo no debe superar los 255 caracteres',
|
||||||
|
'changelog.string' => 'El changelog debe ser una cadena de texto',
|
||||||
|
'changelog.max' => 'El changelog no debe superar los 255 caracteres',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/Http/Requests/Repuve/CancelConstanciaRequest.php
Normal file
57
app/Http/Requests/Repuve/CancelConstanciaRequest.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CancelConstanciaRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->hasPermissionTo('cancellations.cancel_constancia');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'new_folio' => 'required|string',
|
||||||
|
'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id',
|
||||||
|
'cancellation_observations' => 'nullable|string',
|
||||||
|
'new_tag_number' => 'nullable|exists:tags,tag_number',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'record_id.exists' => 'El expediente especificado no existe.',
|
||||||
|
|
||||||
|
'cancellation_reason_id.required' => 'El motivo de cancelación es obligatorio.',
|
||||||
|
'cancellation_reason_id.exists' => 'El motivo de cancelación no es válido.',
|
||||||
|
|
||||||
|
'new_tag_number.exists' => 'El nuevo tag no existe',
|
||||||
|
'folio.required_with' => 'El folio es requerido cuando se proporciona un nuevo tag',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom attributes for validator errors.
|
||||||
|
*/
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'record_id' => 'id del expediente',
|
||||||
|
'cancellation_reason' => 'motivo de cancelación',
|
||||||
|
'cancellation_observations' => 'observaciones',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CatalogCancellationReasonStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => ['required', 'string', 'unique:catalog_cancellation_reasons,code'],
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
'applies_to' => ['required', 'in:cancelacion,sustitucion,ambos'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code.required' => 'El código es obligatorio.',
|
||||||
|
'code.unique' => 'El código ya existe.',
|
||||||
|
'name.required' => 'El nombre es obligatorio.',
|
||||||
|
'applies_to.required' => 'El tipo de aplicación es obligatorio.',
|
||||||
|
'applies_to.in' => 'El tipo de aplicación debe ser: cancelacion, sustitucion o ambos.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => 'código',
|
||||||
|
'name' => 'nombre',
|
||||||
|
'description' => 'descripción',
|
||||||
|
'applies_to' => 'aplica a',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CatalogCancellationReasonUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
'applies_to' => ['required', 'in:cancelacion,sustitucion,ambos'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'El nombre es obligatorio.',
|
||||||
|
'applies_to.required' => 'El tipo de aplicación es obligatorio.',
|
||||||
|
'applies_to.in' => 'El tipo de aplicación debe ser: cancelacion, sustitucion o ambos.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'nombre',
|
||||||
|
'description' => 'descripción',
|
||||||
|
'applies_to' => 'aplica a',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Http/Requests/Repuve/CatalogNameImgStoreRequest.php
Normal file
26
app/Http/Requests/Repuve/CatalogNameImgStoreRequest.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CatalogNameImgStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('catalogs.name_img.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'El nombre es requerido',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Http/Requests/Repuve/CatalogNameImgUpdateRequest.php
Normal file
26
app/Http/Requests/Repuve/CatalogNameImgUpdateRequest.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CatalogNameImgUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('catalogs.name_img.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'names' => ['required'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'names.required' => 'El nombre es requerido',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Http/Requests/Repuve/DeviceStoreRequest.php
Normal file
38
app/Http/Requests/Repuve/DeviceStoreRequest.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DeviceStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('devices.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'brand' => ['required', 'string', 'max:255'],
|
||||||
|
'serie' => ['required', 'string', 'unique:devices,serie', 'max:255'],
|
||||||
|
'mac_address' => ['required', 'string', 'unique:devices,mac_address', 'regex:/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/'],
|
||||||
|
'module_id' => ['required', 'exists:modules,id'],
|
||||||
|
'user_id' => ['nullable', 'array'],
|
||||||
|
'user_id.*' => ['exists:users,id'],
|
||||||
|
'status' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'brand.required' => 'La marca del dispositivo es requerida',
|
||||||
|
'serie.required' => 'El número de serie del dispositivo es requerido',
|
||||||
|
'serie.unique' => 'El número de serie ya está registrado',
|
||||||
|
'mac_address.required' => 'La dirección MAC es requerida',
|
||||||
|
'mac_address.unique' => 'La dirección MAC ya está registrada',
|
||||||
|
'mac_address.regex' => 'La dirección MAC debe tener un formato válido (Ej: 00:1B:44:11:3A:B7)',
|
||||||
|
'module_id.required' => 'El módulo asignado es requerido',
|
||||||
|
'user_id.array' => 'Los usuarios autorizados deben ser un array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Http/Requests/Repuve/DeviceUpdateRequest.php
Normal file
35
app/Http/Requests/Repuve/DeviceUpdateRequest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DeviceUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('devices.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'brand' => ['nullable', 'string', 'max:255'],
|
||||||
|
'serie' => ['nullable', 'string', 'max:255'],
|
||||||
|
'mac_address' => ['nullable', 'string', 'regex:/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/'],
|
||||||
|
'module_id' => ['nullable', 'exists:modules,id'],
|
||||||
|
'user_id' => ['nullable', 'array'],
|
||||||
|
'user_id.*' => ['exists:users,id'],
|
||||||
|
'status' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'brand.required' => 'La marca del dispositivo es requerida',
|
||||||
|
'serie.required' => 'El número de serie del dispositivo es requerido',
|
||||||
|
'module_id.required' => 'El módulo asignado es requerido',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Http/Requests/Repuve/FileStoreRequest.php
Normal file
30
app/Http/Requests/Repuve/FileStoreRequest.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class FileStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'El nombre es obligatorio',
|
||||||
|
'file.required' => 'El archivo es obligatorio',
|
||||||
|
'file.mimes' => 'El archivo debe ser de tipo: jpeg, png, jpg',
|
||||||
|
'file.max' => 'El archivo no debe superar los 10MB',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Http/Requests/Repuve/ModuleStoreRequest.php
Normal file
48
app/Http/Requests/Repuve/ModuleStoreRequest.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ModuleStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('modules.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255', Rule::unique('modules', 'name')],
|
||||||
|
'responsible_id' => ['required', 'exists:users,id'],
|
||||||
|
'municipality_id' => 'required|exists:municipalities,id',
|
||||||
|
'address' => ['required', 'string', 'max:255'],
|
||||||
|
|
||||||
|
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||||
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
'status' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'El nombre del módulo es requerido',
|
||||||
|
'name.string' => 'El nombre debe ser una cadena de texto',
|
||||||
|
'name.max' => 'El nombre no debe superar los 255 caracteres',
|
||||||
|
'name.unique' => 'Ya existe un módulo con ese nombre.',
|
||||||
|
'municipality_id.required' => 'El municipio es requerido',
|
||||||
|
'address.required' => 'La dirección es requerida',
|
||||||
|
'address.string' => 'La dirección debe ser una cadena de texto',
|
||||||
|
'address.max' => 'La dirección no debe superar los 255 caracteres',
|
||||||
|
|
||||||
|
'longitude.numeric' => 'La longitud debe ser un número',
|
||||||
|
'longitude.between' => 'La longitud debe estar entre -180 y 180',
|
||||||
|
'latitude.numeric' => 'La latitud debe ser un número',
|
||||||
|
'latitude.between' => 'La latitud debe estar entre -90 y 90',
|
||||||
|
'status.boolean' => 'El status debe ser un valor booleano',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Http/Requests/Repuve/ModuleUpdateRequest.php
Normal file
49
app/Http/Requests/Repuve/ModuleUpdateRequest.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ModuleUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('modules.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['nullable', 'string', 'max:50', Rule::unique('modules', 'name')->ignore($this->route('module'))],
|
||||||
|
'municipality_id' => ['nullable', 'integer', 'exists:municipalities,id'],
|
||||||
|
'responsible_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'address' => ['nullable', 'string', 'max:250'],
|
||||||
|
|
||||||
|
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||||
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
'status' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.string' => 'El nombre debe ser texto',
|
||||||
|
'name.unique' => 'Ya existe un módulo con ese nombre.',
|
||||||
|
'name.max' => 'El nombre no debe superar los 50 caracteres',
|
||||||
|
'municipality_id.integer' => 'El municipio debe ser un número entero',
|
||||||
|
'municipality_id.exists' => 'El municipio seleccionado no existe',
|
||||||
|
'responsible_id.integer' => 'El responsable debe ser un número entero',
|
||||||
|
'responsible_id.exists' => 'El responsable seleccionado no existe',
|
||||||
|
'address.string' => 'La dirección debe ser texto',
|
||||||
|
'address.max' => 'La dirección no debe superar los 50 caracteres',
|
||||||
|
|
||||||
|
'longitude.numeric' => 'La longitud debe ser un número',
|
||||||
|
'longitude.between' => 'La longitud debe estar entre -180 y 180',
|
||||||
|
'latitude.numeric' => 'La latitud debe ser un número',
|
||||||
|
'latitude.between' => 'La latitud debe estar entre -90 y 90',
|
||||||
|
'status.boolean' => 'El status debe ser un valor booleano',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Http/Requests/Repuve/PackageStoreRequest.php
Normal file
41
app/Http/Requests/Repuve/PackageStoreRequest.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?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', 'integer', 'min:1'],
|
||||||
|
'ending_page' => ['required', 'integer', 'min:1', '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.integer' => 'La página inicial debe ser un número',
|
||||||
|
'starting_page.min' => 'La página inicial debe ser al menos 1',
|
||||||
|
|
||||||
|
'ending_page.required' => 'La página final es requerida',
|
||||||
|
'ending_page.integer' => 'La página final debe ser un número',
|
||||||
|
'ending_page.min' => 'La página final debe ser al menos 1',
|
||||||
|
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Http/Requests/Repuve/PackageUpdateRequest.php
Normal file
41
app/Http/Requests/Repuve/PackageUpdateRequest.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class PackageUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('packages.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lot' => ['sometimes', 'string'],
|
||||||
|
'box_number' => ['sometimes', 'integer'],
|
||||||
|
'starting_page' => ['sometimes', 'integer', 'min:1'],
|
||||||
|
'ending_page' => ['sometimes', 'integer', 'min:1', 'gte:starting_page'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lot.required' => 'El lote es requerido',
|
||||||
|
|
||||||
|
'box_number.required' => 'El número de caja es requerido',
|
||||||
|
|
||||||
|
'starting_page.required' => 'La página inicial es requerida',
|
||||||
|
'starting_page.integer' => 'La página inicial debe ser un número',
|
||||||
|
'starting_page.min' => 'La página inicial debe ser al menos 1',
|
||||||
|
|
||||||
|
'ending_page.required' => 'La página final es requerida',
|
||||||
|
'ending_page.integer' => 'La página final debe ser un número',
|
||||||
|
'ending_page.min' => 'La página final debe ser al menos 1',
|
||||||
|
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Requests/Repuve/TagStoreRequest.php
Normal file
44
app/Http/Requests/Repuve/TagStoreRequest.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TagStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('tags.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => ['required', 'string', 'max:8'],
|
||||||
|
'package_id' => ['required', 'integer', 'exists:packages,id'],
|
||||||
|
'tag_number' => ['nullable', 'string', 'min:32', 'max:32'],
|
||||||
|
'module_id' => ['nullable', 'integer', 'exists:modules,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio.required' => 'El folio es obligatorio.',
|
||||||
|
'folio.max' => 'El folio no puede tener más de 8 caracteres.',
|
||||||
|
'package_id.required' => 'La caja es obligatoria.',
|
||||||
|
'package_id.exists' => 'La caja seleccionada no existe.',
|
||||||
|
'tag_number.min' => 'El número de constancia debe tener exactamente 32 caracteres.',
|
||||||
|
'tag_number.max' => 'El número de constancia debe tener exactamente 32 caracteres.',
|
||||||
|
'module_id.exists' => 'El módulo seleccionado no existe.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => 'folio',
|
||||||
|
'package_id' => 'caja',
|
||||||
|
'tag_number' => 'número de constancia',
|
||||||
|
'module_id' => 'módulo',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Http/Requests/Repuve/TagUpdateRequest.php
Normal file
45
app/Http/Requests/Repuve/TagUpdateRequest.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TagUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('tags.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => ['sometimes', 'string', 'max:8'],
|
||||||
|
'tag_number' => ['nullable', 'string', 'min:32', 'max:32'],
|
||||||
|
'package_id' => ['sometimes', 'integer', 'exists:packages,id'],
|
||||||
|
'module_id' => ['nullable', 'integer', 'exists:modules,id'],
|
||||||
|
'status_id' => ['sometimes', 'integer', 'exists:catalog_tag_status,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio.max' => 'El folio no puede tener más de 8 caracteres.',
|
||||||
|
'tag_number.min' => 'El número de constancia debe tener exactamente 32 caracteres.',
|
||||||
|
'tag_number.max' => 'El número de constancia debe tener exactamente 32 caracteres.',
|
||||||
|
'package_id.exists' => 'La caja seleccionada no existe.',
|
||||||
|
'module_id.exists' => 'El módulo seleccionado no existe.',
|
||||||
|
'status_id.exists' => 'El estado seleccionado no existe.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => 'folio',
|
||||||
|
'tag_number' => 'número de constancia',
|
||||||
|
'package_id' => 'caja',
|
||||||
|
'module_id' => 'módulo',
|
||||||
|
'status_id' => 'estado',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Requests/Repuve/VehicleStoreRequest.php
Normal file
43
app/Http/Requests/Repuve/VehicleStoreRequest.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class VehicleStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => ['required', 'string', 'max:50'],
|
||||||
|
'tag_number' => ['required', 'string'],
|
||||||
|
'placa' => ['required', 'string', 'max:30'],
|
||||||
|
'telefono' => ['required', 'string', 'max:11'],
|
||||||
|
'files' => ['nullable', 'array', 'min:1'],
|
||||||
|
'files.*' => ['file', 'mimes:jpeg,png,jpg', 'max:10240'],
|
||||||
|
'name_id' => ['nullable', 'array', 'min:1'],
|
||||||
|
'name_id.*' => ['nullable', 'integer', 'exists:catalog_name_img,id']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio.required' => 'El folio es requerido',
|
||||||
|
'folio.string' => 'El folio debe ser una cadena de texto',
|
||||||
|
'tag_number.required' => 'El tag_number es requerido',
|
||||||
|
'placa.required' => 'La placa es requerida',
|
||||||
|
'placa.string' => 'La placa debe ser una cadena de texto',
|
||||||
|
'telefono.required' => 'El teléfono es requerido',
|
||||||
|
'telefono.max' => 'El teléfono no debe superar los 10 caracteres',
|
||||||
|
'files.array' => 'Los archivos deben ser un array',
|
||||||
|
'files.*.file' => 'Cada elemento debe ser un archivo válido',
|
||||||
|
'files.*.mimes' => 'Los archivos deben ser de tipo: jpeg, png, jpg, pdf',
|
||||||
|
'files.*.max' => 'Cada archivo no debe superar los 10MB',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Http/Requests/Repuve/VehicleUpdateRequest.php
Normal file
90
app/Http/Requests/Repuve/VehicleUpdateRequest.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Repuve;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class VehicleUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'folio' => 'nullable|string|max:50|unique:records,folio,' . $this->route('id'),
|
||||||
|
// --- DATOS DEL VEHÍCULO ---
|
||||||
|
'vehicle.placa' => 'nullable|string|max:20',
|
||||||
|
'vehicle.marca' => 'nullable|string|max:100',
|
||||||
|
'vehicle.linea' => 'nullable|string|max:100',
|
||||||
|
'vehicle.sublinea' => 'nullable|string|max:100',
|
||||||
|
'vehicle.modelo' => 'nullable|string',
|
||||||
|
'vehicle.color' => 'nullable|string|max:50',
|
||||||
|
'vehicle.numero_motor' => 'nullable|string|max:50',
|
||||||
|
'vehicle.clase_veh' => 'nullable|string|max:50',
|
||||||
|
'vehicle.tipo_servicio' => 'nullable|string|max:50',
|
||||||
|
'vehicle.rfv' => 'nullable|string|max:50',
|
||||||
|
'vehicle.rfc' => 'nullable|string|max:13',
|
||||||
|
'vehicle.ofcexpedicion' => 'nullable|string|max:100',
|
||||||
|
'vehicle.fechaexpedicion' => ['nullable', 'date_format:d/m/Y'],
|
||||||
|
'vehicle.tipo_veh' => 'nullable|string|max:50',
|
||||||
|
'vehicle.numptas' => 'nullable|string',
|
||||||
|
'vehicle.observac' => 'nullable|string|max:500',
|
||||||
|
'vehicle.cve_vehi' => 'nullable|string|max:50',
|
||||||
|
'vehicle.nrpv' => 'nullable|string|max:50',
|
||||||
|
'vehicle.tipo_mov' => 'nullable|string|max:50',
|
||||||
|
|
||||||
|
// --- DATOS DEL TAG ---
|
||||||
|
'tag.tag_number' => 'nullable|string|max:32',
|
||||||
|
|
||||||
|
// --- DATOS DEL PROPIETARIO ---
|
||||||
|
'owner.name' => 'nullable|string|max:100',
|
||||||
|
'owner.paternal' => 'nullable|string|max:100',
|
||||||
|
'owner.maternal' => 'nullable|string|max:100',
|
||||||
|
'owner.rfc' => 'nullable|string|max:13',
|
||||||
|
'owner.curp' => 'nullable|string|max:18',
|
||||||
|
'owner.address' => 'nullable|string|max:255',
|
||||||
|
'owner.tipopers' => 'nullable|boolean',
|
||||||
|
'owner.pasaporte' => 'nullable|string|max:20',
|
||||||
|
'owner.licencia' => 'nullable|string|max:20',
|
||||||
|
'owner.ent_fed' => 'nullable|string|max:50',
|
||||||
|
'owner.munic' => 'nullable|string|max:100',
|
||||||
|
'owner.callep' => 'nullable|string|max:100',
|
||||||
|
'owner.num_ext' => 'nullable|string|max:10',
|
||||||
|
'owner.num_int' => 'nullable|string|max:10',
|
||||||
|
'owner.colonia' => 'nullable|string|max:100',
|
||||||
|
'owner.cp' => 'nullable|string|max:5',
|
||||||
|
'owner.telefono' => 'nullable|string|max:15',
|
||||||
|
|
||||||
|
// --- ARCHIVOS ---
|
||||||
|
'files' => 'nullable|array|min:1',
|
||||||
|
'files.*' => 'file|mimes:jpeg,png,jpg|max:2048',
|
||||||
|
'name_id' => 'nullable|array',
|
||||||
|
'name_id.*' => 'integer|exists:catalog_name_img,id',
|
||||||
|
'observations' => 'nullable|array',
|
||||||
|
'observations.*' => 'nullable|string|max:500',
|
||||||
|
'delete_files' => 'nullable|array',
|
||||||
|
'delete_files.*' => 'integer|exists:files,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'vehicle.modelo.string' => 'El modelo debe ser texto',
|
||||||
|
'vehicle.numptas.string' => 'El número de puertas debe ser texto',
|
||||||
|
'owner.tipopers.boolean' => 'El tipo de persona debe ser física o Moral',
|
||||||
|
'owner.cp.max' => 'El código postal debe tener máximo 5 caracteres',
|
||||||
|
'files.*.mimes' => 'Solo se permiten archivos JPG, PNG o JPEG',
|
||||||
|
'files.*.max' => 'El archivo no debe superar 2MB',
|
||||||
|
'observations.*.max' => 'La observación no debe superar 120 caracteres',
|
||||||
|
'delete_files.*.exists' => 'El archivo a eliminar no existe',
|
||||||
|
'folio.unique' => 'El folio ya existe en el sistema',
|
||||||
|
'folio.max' => 'El folio no puede exceder 50 caracteres',
|
||||||
|
'tag.tag_number.max' => 'El tag_number no puede exceder 32 caracteres',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ class RoleStoreRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->hasPermissionTo('roles.create');
|
return auth()->user()->can('roles.create');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Actualizar rol
|
* Actualizar rol
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class RoleUpdateRequest extends FormRequest
|
class RoleUpdateRequest extends FormRequest
|
||||||
@ -21,7 +21,7 @@ class RoleUpdateRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->hasPermissionTo('roles.edit');
|
return auth()->user()->can('roles.edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,8 +39,10 @@ public function rules(): array
|
|||||||
*/
|
*/
|
||||||
protected function passedValidation()
|
protected function passedValidation()
|
||||||
{
|
{
|
||||||
$this->merge([
|
if(!in_array($this->route('role')->id, [1, 2])) {
|
||||||
'name' => Str::slug($this->description),
|
$this->merge([
|
||||||
]);
|
'name' => Str::slug($this->description),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,15 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'email', 'exists:users,email']
|
'username' => ['required', 'string', 'exists:users,username']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username.required' => 'El nombre de usuario es requerido',
|
||||||
|
'username.exists' => 'El usuario no existe',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,11 +32,11 @@ public function rules(): array
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'paternal' => ['required', 'string', 'max:255'],
|
'paternal' => ['required', 'string', 'max:255'],
|
||||||
'maternal' => ['required', 'string', 'max:255'],
|
'maternal' => ['required', 'string', 'max:255'],
|
||||||
'email' => [
|
'username' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
'email',
|
|
||||||
'max:255',
|
'max:255',
|
||||||
|
'alpha_dash',
|
||||||
Rule::unique('users')->ignore(auth()->user()->id),
|
Rule::unique('users')->ignore(auth()->user()->id),
|
||||||
],
|
],
|
||||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Descripción
|
* Descripción
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class UserActivityRequest extends FormRequest
|
class UserActivityRequest extends FormRequest
|
||||||
@ -19,7 +19,7 @@ class UserActivityRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return auth()->user()->can('activities.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Almacenar usuario
|
* Almacenar usuario
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class UserStoreRequest extends FormRequest
|
class UserStoreRequest extends FormRequest
|
||||||
@ -33,10 +33,19 @@ public function rules(): array
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'paternal' => ['required', 'string', 'max:255'],
|
'paternal' => ['required', 'string', 'max:255'],
|
||||||
'maternal' => ['required', 'string', 'max:255'],
|
'maternal' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
'username' => ['required', 'string', 'max:255', 'unique:users', 'alpha_dash'],
|
||||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||||
'password' => ['required', 'string', 'min:8'],
|
'password' => ['required', 'string', 'min:8'],
|
||||||
'roles' => ['nullable', 'array']
|
'roles' => ['nullable', 'array']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username.required' => 'El nombre de usuario es requerido',
|
||||||
|
'username.unique' => 'El nombre de usuario ya está en uso',
|
||||||
|
'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,18 @@ public function rules(): array
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'paternal' => ['required', 'string', 'max:255'],
|
'paternal' => ['required', 'string', 'max:255'],
|
||||||
'maternal' => ['required', 'string', 'max:255'],
|
'maternal' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($this->route('user'))],
|
'username' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('users')->ignore($this->route('user'))],
|
||||||
'phone' => ['nullable', 'numeric', 'digits:10'],
|
'phone' => ['nullable', 'numeric', 'digits:10'],
|
||||||
'roles' => ['nullable', 'array']
|
'roles' => ['nullable', 'array']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username.required' => 'El nombre de usuario es requerido',
|
||||||
|
'username.unique' => 'El nombre de usuario ya está en uso',
|
||||||
|
'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
app/Jobs/ProcessRepuveResponse.php
Normal file
117
app/Jobs/ProcessRepuveResponse.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Error;
|
||||||
|
use App\Models\Record;
|
||||||
|
use App\Services\RepuveService;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class ProcessRepuveResponse implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public int $tries = 2;
|
||||||
|
public int $timeout = 250;
|
||||||
|
public array $backoff = [30];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear instancia del trabajo
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public int $recordId,
|
||||||
|
public array $responseData
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecutar el trabajo
|
||||||
|
*/
|
||||||
|
public function handle(RepuveService $repuveService): void
|
||||||
|
{
|
||||||
|
$record = Record::findOrFail($this->recordId);
|
||||||
|
|
||||||
|
Log::info('ProcessRepuveResponse: Enviando inscripción a REPUVE Nacional...', [
|
||||||
|
'niv' => $this->responseData['niv'] ?? 'N/A',
|
||||||
|
'placa' => $this->responseData['placa'] ?? 'N/A',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$apiResponse = $repuveService->inscribirVehiculo($this->responseData);
|
||||||
|
|
||||||
|
Log::info('ProcessRepuveResponse: Respuesta recibida de REPUVE', [
|
||||||
|
'has_error' => $apiResponse['has_error'],
|
||||||
|
'error_code' => $apiResponse['error_code'] ?? null,
|
||||||
|
'timestamp' => $apiResponse['timestamp'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if($apiResponse['has_error']){
|
||||||
|
$error = Error::where('code', $apiResponse['error_code'])->first();
|
||||||
|
|
||||||
|
Log::error('ProcessRepuveResponse: Error en respuesta REPUVE', [
|
||||||
|
'error_code' => $apiResponse['error_code'],
|
||||||
|
'error_message' => $apiResponse['error_message'] ?? 'Sin mensaje',
|
||||||
|
'error_found_in_db' => $error ? 'Sí' : 'No',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$record->update([
|
||||||
|
'error_id' => $error?->id,
|
||||||
|
'api_response' => $apiResponse,
|
||||||
|
'error_occurred_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::warning('ProcessRepuveResponse: Record actualizado con error', [
|
||||||
|
'record_id' => $record->id,
|
||||||
|
'error_id' => $error?->id,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$record->update([
|
||||||
|
'error_id' => null,
|
||||||
|
'api_response' => $apiResponse,
|
||||||
|
'error_occurred_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::critical('ProcessRepuveResponse: Job FALLÓ después de todos los intentos', [
|
||||||
|
'record_id' => $this->recordId,
|
||||||
|
'exception_class' => get_class($exception),
|
||||||
|
'exception_message' => $exception->getMessage(),
|
||||||
|
'exception_file' => $exception->getFile(),
|
||||||
|
'exception_line' => $exception->getLine(),
|
||||||
|
'attempts' => $this->attempts(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$record = Record::find($this->recordId);
|
||||||
|
if($record){
|
||||||
|
Log::info('ProcessRepuveResponse: Buscando error genérico código -1');
|
||||||
|
|
||||||
|
$error = Error::where('code', '-1')->first();
|
||||||
|
|
||||||
|
if(!$error){
|
||||||
|
Log::warning('ProcessRepuveResponse: Error código -1 NO encontrado en BD');
|
||||||
|
}
|
||||||
|
|
||||||
|
$record->update([
|
||||||
|
'error_id' => $error?->id,
|
||||||
|
'api_response' => [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => $exception->getMessage(),
|
||||||
|
],
|
||||||
|
'error_occurred_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::error('ProcessRepuveResponse: Record actualizado con error crítico', [
|
||||||
|
'record_id' => $record->id,
|
||||||
|
'error_id' => $error?->id,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Log::error('ProcessRepuveResponse: Record NO encontrado', [
|
||||||
|
'record_id' => $this->recordId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/ApkLog.php
Normal file
32
app/Models/ApkLog.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class ApkLog extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'apk_version_id',
|
||||||
|
'downloaded_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function apkVersion()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ApkVersion::class, 'apk_version_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloader()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'downloaded_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/ApkVersion.php
Normal file
25
app/Models/ApkVersion.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ApkVersion extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'file_name',
|
||||||
|
'uploaded_by',
|
||||||
|
'changelog',
|
||||||
|
'path',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function uploader()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploaded_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logs()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ApkLog::class, 'apk_version_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/CatalogCancellationReason.php
Normal file
42
app/Models/CatalogCancellationReason.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CatalogCancellationReason extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'applies_to'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener razones para cancelación
|
||||||
|
*/
|
||||||
|
public function scopeForCancellation($query)
|
||||||
|
{
|
||||||
|
return $query->whereIn('applies_to', ['cancelacion', 'ambos']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener razones para sustitución
|
||||||
|
*/
|
||||||
|
public function scopeForSubstitution($query)
|
||||||
|
{
|
||||||
|
return $query->whereIn('applies_to', ['sustitucion', 'ambos']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs que usan esta razón
|
||||||
|
*/
|
||||||
|
public function vehicleTagLogs()
|
||||||
|
{
|
||||||
|
return $this->hasMany(VehicleTagLog::class, 'cancellation_reason_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Models/CatalogNameImg.php
Normal file
28
app/Models/CatalogNameImg.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class CatalogNameImg extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'catalog_name_img';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function files()
|
||||||
|
{
|
||||||
|
return $this->hasMany(File::class, 'name_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Models/CatalogTagStatus.php
Normal file
62
app/Models/CatalogTagStatus.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catálogo de estatus de tags
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class CatalogTagStatus extends Model
|
||||||
|
{
|
||||||
|
// Constantes de códigos de estatus
|
||||||
|
const CODE_AVAILABLE = 'available';
|
||||||
|
const CODE_ASSIGNED = 'assigned';
|
||||||
|
const CODE_CANCELLED = 'cancelled';
|
||||||
|
|
||||||
|
protected $table = 'catalog_tag_status';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'active' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags que tienen este estatus
|
||||||
|
*/
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Tag::class, 'status_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope para obtener solo estatus activos
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope para buscar por código
|
||||||
|
*/
|
||||||
|
public function scopeByCode($query, string $code)
|
||||||
|
{
|
||||||
|
return $query->where('code', $code);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Models/Device.php
Normal file
45
app/Models/Device.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Device extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'brand',
|
||||||
|
'serie',
|
||||||
|
'mac_address',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modules()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Module::class, 'device_module')
|
||||||
|
->withPivot('status')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deviceModules()
|
||||||
|
{
|
||||||
|
return $this->hasMany(DeviceModule::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeModules()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Module::class, 'device_module')
|
||||||
|
->wherePivot('status', true)
|
||||||
|
->withPivot('status')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/DeviceModule.php
Normal file
42
app/Models/DeviceModule.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DeviceModule extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'device_module';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'device_id',
|
||||||
|
'module_id',
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function device()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Device::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function module()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Module::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/Error.php
Normal file
23
app/Models/Error.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Error extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'type'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function records()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Record::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Models/File.php
Normal file
43
app/Models/File.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class File extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name_id',
|
||||||
|
'path',
|
||||||
|
'md5',
|
||||||
|
'observations',
|
||||||
|
'record_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'url',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function record()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Record::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function catalogName()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogNameImg::class, 'name_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => Storage::disk('public')->url($this->path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Models/Module.php
Normal file
69
app/Models/Module.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Module extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'responsible_id',
|
||||||
|
'municipality_id',
|
||||||
|
'address',
|
||||||
|
'longitude',
|
||||||
|
'latitude',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'longitude' => 'decimal:8',
|
||||||
|
'latitude' => 'decimal:8',
|
||||||
|
'status' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function municipality()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Municipality::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tags(){
|
||||||
|
return $this->hasMany(Tag::class, 'module_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function devices()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Device::class, 'device_module')
|
||||||
|
->withPivot('status')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deviceModules()
|
||||||
|
{
|
||||||
|
return $this->hasMany(DeviceModule::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeDevices()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Device::class, 'device_module')
|
||||||
|
->wherePivot('status', true)
|
||||||
|
->withPivot('status')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function responsible()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'responsible_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class, 'module_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Models/Municipality.php
Normal file
27
app/Models/Municipality.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descripción
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class Municipality extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function modules()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Module::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Models/Owner.php
Normal file
53
app/Models/Owner.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Owner extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'paternal',
|
||||||
|
'maternal',
|
||||||
|
'rfc',
|
||||||
|
'curp',
|
||||||
|
'address',
|
||||||
|
'tipopers',
|
||||||
|
'pasaporte',
|
||||||
|
'licencia',
|
||||||
|
'ent_fed',
|
||||||
|
'munic',
|
||||||
|
'callep',
|
||||||
|
'num_ext',
|
||||||
|
'num_int',
|
||||||
|
'colonia',
|
||||||
|
'cp',
|
||||||
|
'telefono',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'full_name',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function fullName(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => trim("{$this->name} {$this->paternal} {$this->maternal}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vehicles()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Vehicle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function municipality()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Municipality::class, 'munic', 'code');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Models/Package.php
Normal file
34
app/Models/Package.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Package extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'lot',
|
||||||
|
'box_number',
|
||||||
|
'starting_page',
|
||||||
|
'ending_page',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'starting_page' => 'integer',
|
||||||
|
'ending_page' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Tag::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
app/Models/Record.php
Normal file
85
app/Models/Record.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Record extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'folio',
|
||||||
|
'vehicle_id',
|
||||||
|
'user_id',
|
||||||
|
'module_id',
|
||||||
|
'error_id',
|
||||||
|
'api_response',
|
||||||
|
'error_occurred_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'api_response' => 'array',
|
||||||
|
'error_occurred_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function vehicle()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Vehicle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function files()
|
||||||
|
{
|
||||||
|
return $this->hasMany(File::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function error()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Error::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function module()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Module::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vehicleTagLog()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
VehicleTagLog::class,
|
||||||
|
Vehicle::class,
|
||||||
|
'id',
|
||||||
|
'vehicle_id',
|
||||||
|
'vehicle_id',
|
||||||
|
'id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtra registros por módulo del usuario
|
||||||
|
* Admin y developer pueden ver todos los registros
|
||||||
|
* Otros usuarios solo ven registros de su módulo
|
||||||
|
*/
|
||||||
|
public function scopeForUserModule($query, $user)
|
||||||
|
{
|
||||||
|
$isAdminOrDeveloper = $user->hasRole(['admin', 'developer'])
|
||||||
|
|| $user->hasRole(['admin', 'developer'], 'api')
|
||||||
|
|| $user->roles()->whereIn('name', ['admin', 'developer'])->exists();
|
||||||
|
|
||||||
|
if ($isAdminOrDeveloper) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($user->module_id)) {
|
||||||
|
return $query->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('module_id', $user->module_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Models/ScanHistory.php
Normal file
36
app/Models/ScanHistory.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ScanHistory extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'scan_history';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'tag_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con User
|
||||||
|
* Un escaneo pertenece a un usuario
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con Tag
|
||||||
|
* Un escaneo pertenece a una etiqueta
|
||||||
|
*/
|
||||||
|
public function tag()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tag::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/Models/Tag.php
Normal file
133
app/Models/Tag.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Tag extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
// Constantes de status
|
||||||
|
const STATUS_AVAILABLE = 'available';
|
||||||
|
const STATUS_ASSIGNED = 'assigned';
|
||||||
|
const STATUS_CANCELLED = 'cancelled';
|
||||||
|
const STATUS_DAMAGED = 'damaged';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'folio',
|
||||||
|
'tag_number',
|
||||||
|
'vehicle_id',
|
||||||
|
'package_id',
|
||||||
|
'module_id',
|
||||||
|
'status_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function vehicle()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Vehicle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function package()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Package::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogTagStatus::class, 'status_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function module(){
|
||||||
|
return $this->belongsTo(Module::class, 'module_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vehicleTagLogs()
|
||||||
|
{
|
||||||
|
return $this->hasMany(VehicleTagLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scanHistories()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ScanHistory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancellationLogs()
|
||||||
|
{
|
||||||
|
return $this->hasMany(TagCancellationLog::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar tag como asignado a un vehículo
|
||||||
|
*/
|
||||||
|
public function markAsAssigned(int $vehicleId, string $folio): void
|
||||||
|
{
|
||||||
|
$statusAssigned = CatalogTagStatus::where('code', self::STATUS_ASSIGNED)->first();
|
||||||
|
|
||||||
|
$this->update([
|
||||||
|
'vehicle_id' => $vehicleId,
|
||||||
|
'folio' => $folio,
|
||||||
|
'status_id' => $statusAssigned->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar tag como cancelado
|
||||||
|
*/
|
||||||
|
public function markAsCancelled(): void
|
||||||
|
{
|
||||||
|
$statusCancelled = CatalogTagStatus::where('code', self::STATUS_CANCELLED)->first();
|
||||||
|
|
||||||
|
$this->update([
|
||||||
|
'status_id' => $statusCancelled->id,
|
||||||
|
'vehicle_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar tag como dañado
|
||||||
|
*/
|
||||||
|
public function markAsDamaged(): void
|
||||||
|
{
|
||||||
|
$statusDamaged = CatalogTagStatus::where('code', self::STATUS_DAMAGED)->first();
|
||||||
|
|
||||||
|
$this->update([
|
||||||
|
'status_id' => $statusDamaged->id,
|
||||||
|
'vehicle_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el tag está disponible
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return $this->status->code === self::STATUS_AVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el tag está asignado
|
||||||
|
*/
|
||||||
|
public function isAssigned(): bool
|
||||||
|
{
|
||||||
|
return $this->status->code === self::STATUS_ASSIGNED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el tag está cancelado
|
||||||
|
*/
|
||||||
|
public function isCancelled(): bool
|
||||||
|
{
|
||||||
|
return $this->status->code === self::STATUS_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el tag está dañado
|
||||||
|
*/
|
||||||
|
public function isDamaged(): bool
|
||||||
|
{
|
||||||
|
return $this->status->code === self::STATUS_DAMAGED;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
app/Models/TagCancellationLog.php
Normal file
51
app/Models/TagCancellationLog.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log de cancelación de tags no asignados
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class TagCancellationLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'tag_cancellation_logs';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tag_id',
|
||||||
|
'cancellation_reason_id',
|
||||||
|
'cancellation_observations',
|
||||||
|
'cancellation_at',
|
||||||
|
'cancelled_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'cancellation_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relaciones
|
||||||
|
public function tag()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tag::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancellationReason()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelledBy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'cancelled_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
<?php namespace App\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
*/
|
*/
|
||||||
@ -19,9 +22,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Modelo de usuario
|
* Modelo de usuario
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
#[ObservedBy([UserObserver::class])]
|
#[ObservedBy([UserObserver::class])]
|
||||||
@ -42,10 +45,10 @@ class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'paternal',
|
'paternal',
|
||||||
'maternal',
|
'maternal',
|
||||||
'email',
|
'username',
|
||||||
'phone',
|
'phone',
|
||||||
'password',
|
'password',
|
||||||
'profile_photo_path',
|
'module_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,6 +57,7 @@ class User extends Authenticatable
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
|
'profile_photo_path'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +66,6 @@ class User extends Authenticatable
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -98,7 +101,7 @@ public function reports()
|
|||||||
public function fullName(): Attribute
|
public function fullName(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
|
get: fn() => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +111,7 @@ public function fullName(): Attribute
|
|||||||
public function lastName(): Attribute
|
public function lastName(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->paternal . ' ' . $this->maternal,
|
get: fn() => $this->paternal . ' ' . $this->maternal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,4 +130,41 @@ public function resetPasswords()
|
|||||||
{
|
{
|
||||||
return $this->hasMany(ResetPassword::class);
|
return $this->hasMany(ResetPassword::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function module()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Module::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preguntar si el usuario es desarrollador
|
||||||
|
*/
|
||||||
|
public function isDeveloper(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(Role::find(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preguntar si el usuario es administrador
|
||||||
|
*/
|
||||||
|
public function isAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(Role::find(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preguntar si el usuario es primario (privilegios elevados)
|
||||||
|
*/
|
||||||
|
public function isPrimary(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(Role::find(1), Role::find(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Módulo del cual el usuario es responsable
|
||||||
|
*/
|
||||||
|
public function responsibleModule()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Module::class, 'responsible_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
app/Models/Vehicle.php
Normal file
61
app/Models/Vehicle.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Vehicle extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'vehicle';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'placa',
|
||||||
|
'niv',
|
||||||
|
'marca',
|
||||||
|
'linea',
|
||||||
|
'sublinea',
|
||||||
|
'modelo',
|
||||||
|
'color',
|
||||||
|
'numero_motor',
|
||||||
|
'clase_veh',
|
||||||
|
'tipo_servicio',
|
||||||
|
'rfv',
|
||||||
|
'ofcexpedicion',
|
||||||
|
'fechaexpedicion',
|
||||||
|
'tipo_veh',
|
||||||
|
'numptas',
|
||||||
|
'observac',
|
||||||
|
'cve_vehi',
|
||||||
|
'nrpv',
|
||||||
|
'tipo_mov',
|
||||||
|
'owner_id',
|
||||||
|
'reporte_robo',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'reporte_robo' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function owner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Owner::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function records()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Record::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tag()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Tag::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vehicleTagLogs()
|
||||||
|
{
|
||||||
|
return $this->hasMany(VehicleTagLog::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
app/Models/VehicleTagLog.php
Normal file
74
app/Models/VehicleTagLog.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class VehicleTagLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'vehicle_tags_logs';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'vehicle_id',
|
||||||
|
'tag_id',
|
||||||
|
'folio_anterior',
|
||||||
|
'action_type',
|
||||||
|
'cancellation_reason_id',
|
||||||
|
'cancellation_observations',
|
||||||
|
'cancellation_at',
|
||||||
|
'cancelled_by',
|
||||||
|
'performed_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'cancellation_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function vehicle() {
|
||||||
|
return $this->belongsTo(Vehicle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tag() {
|
||||||
|
return $this->belongsTo(Tag::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelledBy() {
|
||||||
|
return $this->belongsTo(User::class, 'cancelled_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function performedBy() {
|
||||||
|
return $this->belongsTo(User::class, 'performed_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancellationReason()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogCancellationReason::class, 'cancellation_reason_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInscription()
|
||||||
|
{
|
||||||
|
return $this->action_type === 'sustitucion_primera_vez';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUpdate()
|
||||||
|
{
|
||||||
|
return $this->action_type === 'actualizacion';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSubstitution()
|
||||||
|
{
|
||||||
|
return $this->action_type === 'sustitucion';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCancellation()
|
||||||
|
{
|
||||||
|
return $this->action_type === 'cancelacion';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -23,7 +23,7 @@ public function created(User $user): void
|
|||||||
UserEvent::report(
|
UserEvent::report(
|
||||||
model: $user,
|
model: $user,
|
||||||
event: __FUNCTION__,
|
event: __FUNCTION__,
|
||||||
key: 'email'
|
key: 'username'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ public function updated(User $user): void
|
|||||||
UserEvent::report(
|
UserEvent::report(
|
||||||
model: $user,
|
model: $user,
|
||||||
event: __FUNCTION__,
|
event: __FUNCTION__,
|
||||||
key: 'email',
|
key: 'username',
|
||||||
reportChanges: true
|
reportChanges: true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ public function deleted(User $user): void
|
|||||||
UserEvent::report(
|
UserEvent::report(
|
||||||
model: $user,
|
model: $user,
|
||||||
event: __FUNCTION__,
|
event: __FUNCTION__,
|
||||||
key: 'email'
|
key: 'username'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ public function restored(User $user): void
|
|||||||
UserEvent::report(
|
UserEvent::report(
|
||||||
model: $user,
|
model: $user,
|
||||||
event: __FUNCTION__,
|
event: __FUNCTION__,
|
||||||
key: 'email'
|
key: 'username'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ public function forceDeleted(User $user): void
|
|||||||
UserEvent::report(
|
UserEvent::report(
|
||||||
model: $user,
|
model: $user,
|
||||||
event: __FUNCTION__,
|
event: __FUNCTION__,
|
||||||
key: 'email'
|
key: 'username'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Proveedor de servicios de Telescope
|
* Proveedor de servicios de Telescope
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
||||||
@ -65,7 +65,7 @@ protected function hideSensitiveRequestDetails(): void
|
|||||||
protected function gate(): void
|
protected function gate(): void
|
||||||
{
|
{
|
||||||
Gate::define('viewTelescope', function (User $user) {
|
Gate::define('viewTelescope', function (User $user) {
|
||||||
return $user->hasRole('developer');
|
return $user->isDeveloper();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
app/Services/LogsService.php
Normal file
139
app/Services/LogsService.php
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<?php namespace App\Services;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use SplFileObject;
|
||||||
|
|
||||||
|
class LogsService
|
||||||
|
{
|
||||||
|
private array $sources = [
|
||||||
|
'repuve' => 'logs/repuve-nacional.log',
|
||||||
|
'padron-estatal' => 'logs/padron-estatal.log',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function readRepuve(array $filters): array
|
||||||
|
{
|
||||||
|
return $this->read('repuve', $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readPadronEstatal(array $filters): array
|
||||||
|
{
|
||||||
|
return $this->read('padron-estatal', $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function read(string $source, array $filters): array
|
||||||
|
{
|
||||||
|
$path = storage_path($this->sources[$source]);
|
||||||
|
|
||||||
|
if (!is_file($path) || !is_readable($path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = $this->parseFile($path);
|
||||||
|
|
||||||
|
return $this->applyFilters($entries, $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseFile(string $path): array
|
||||||
|
{
|
||||||
|
$file = new SplFileObject($path, 'r');
|
||||||
|
$entries = [];
|
||||||
|
$current = null;
|
||||||
|
|
||||||
|
while (!$file->eof()) {
|
||||||
|
$line = rtrim((string) $file->fgets(), "\r\n");
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = $this->parseHeader($line);
|
||||||
|
|
||||||
|
if ($header) {
|
||||||
|
if ($current) {
|
||||||
|
$entries[] = $this->sanitize($current);
|
||||||
|
}
|
||||||
|
$current = $header;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current) {
|
||||||
|
$current['message'] .= "\n" . $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current) {
|
||||||
|
$entries[] = $this->sanitize($current);
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($entries, fn($a, $b) => strcmp($b['timestamp'], $a['timestamp']));
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseHeader(string $line): ?array
|
||||||
|
{
|
||||||
|
$pattern = '/^\[(?<dt>[^\]]+)\]\s[^\.\s]+\.(?<level>[A-Z]+):\s(?<message>.*)$/';
|
||||||
|
|
||||||
|
if (!preg_match($pattern, $line, $m)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$date = Carbon::parse($m['dt']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'timestamp' => $date->toDateTimeString(),
|
||||||
|
'level' => strtolower($m['level']),
|
||||||
|
'message' => $m['message'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyFilters(array $entries, array $filters): array
|
||||||
|
{
|
||||||
|
$start = !empty($filters['start_date']) ? Carbon::parse($filters['start_date'])->startOfDay() : null;
|
||||||
|
$end = !empty($filters['end_date']) ? Carbon::parse($filters['end_date'])->endOfDay() : null;
|
||||||
|
$level = $filters['level'] ?? null;
|
||||||
|
|
||||||
|
return array_values(array_filter($entries, function (array $entry) use ($start, $end, $level) {
|
||||||
|
$dt = Carbon::parse($entry['timestamp']);
|
||||||
|
|
||||||
|
if ($start && $dt->lt($start)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end && $dt->gt($end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($level && $entry['level'] !== strtolower($level)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitize(array $entry): array
|
||||||
|
{
|
||||||
|
$text = $entry['message'];
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'/("password"\s*:\s*")[^"]*(")/i' => '$1***$2',
|
||||||
|
'/("token"\s*:\s*")[^"]*(")/i' => '$1***$2',
|
||||||
|
'/(authorization)\s*[:=]\s*([^,\s]+)/i' => '$1=***',
|
||||||
|
'/(<arg0>)(.*?)(<\/arg0>)/i' => '$1***$3',
|
||||||
|
'/(<arg1>)(.*?)(<\/arg1>)/i' => '$1***$3',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern => $replacement) {
|
||||||
|
$text = preg_replace($pattern, $replacement, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry['message'] = $text;
|
||||||
|
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
237
app/Services/PadronEstatalService.php
Normal file
237
app/Services/PadronEstatalService.php
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Exceptions\PadronEstatalException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class PadronEstatalService
|
||||||
|
{
|
||||||
|
private string $soapUrl;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->soapUrl = config('services.padron_estatal.url');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVehiculoByNiv(string $niv): array
|
||||||
|
{
|
||||||
|
return $this->consultarPadron('niv', $niv);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVehiculoByPlaca(string $placa): array
|
||||||
|
{
|
||||||
|
return $this->consultarPadron('placa', $placa);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVehiculoByFolio(string $folio): array
|
||||||
|
{
|
||||||
|
return $this->consultarPadron('folio', $folio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consulta el padrón vehicular estatal
|
||||||
|
*/
|
||||||
|
private function consultarPadron(string $tipo, string $valor): array
|
||||||
|
{
|
||||||
|
$logger = Log::channel('padron_estatal');
|
||||||
|
|
||||||
|
// Construir el Data en formato JSON
|
||||||
|
$data = json_encode([
|
||||||
|
'tipo' => $tipo,
|
||||||
|
'valor' => $valor
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Construir el cuerpo SOAP
|
||||||
|
$soapBody = <<<XML
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://mx/tgc/ConsultaPadronVehicular.wsdl">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<wsdl:getVehiculosRepuve>
|
||||||
|
<Data>{$data}</Data>
|
||||||
|
</wsdl:getVehiculosRepuve>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$logger->info('Consulta al padrón estatal', [
|
||||||
|
'url' => $this->soapUrl,
|
||||||
|
'tipo' => $tipo,
|
||||||
|
'valor' => $valor,
|
||||||
|
]);
|
||||||
|
$logger->debug('SOAP request body', ['body' => $soapBody]);
|
||||||
|
|
||||||
|
// Configurar cURL
|
||||||
|
$ch = curl_init($this->soapUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=utf-8',
|
||||||
|
'SOAPAction: ""',
|
||||||
|
'Content-Length: ' . strlen($soapBody)
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ejecutar la petición
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlInfo = curl_getinfo($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
|
||||||
|
$logger->debug('Respuesta cURL', [
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'total_time' => $curlInfo['total_time'] ?? null,
|
||||||
|
'connect_time' => $curlInfo['connect_time'] ?? null,
|
||||||
|
'namelookup_time' => $curlInfo['namelookup_time'] ?? null,
|
||||||
|
'curl_error' => $error ?: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$logger->error('Error cURL al consultar padrón estatal', ['error' => $error]);
|
||||||
|
throw new PadronEstatalException("Error en la petición al padrón estatal: {$error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
$logger->error('HTTP error al consultar padrón estatal', [
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'response' => $response,
|
||||||
|
]);
|
||||||
|
throw new PadronEstatalException("Error HTTP {$httpCode} al consultar padrón estatal");
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger->debug('SOAP response body', ['body' => $response]);
|
||||||
|
|
||||||
|
// Parsear la respuesta
|
||||||
|
return $this->parsearRespuesta($response);
|
||||||
|
} finally {
|
||||||
|
unset($ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea la respuesta del padrón estatal
|
||||||
|
*/
|
||||||
|
private function parsearRespuesta(string $soapResponse): array
|
||||||
|
{
|
||||||
|
$logger = Log::channel('padron_estatal');
|
||||||
|
|
||||||
|
// Extraer el contenido del tag
|
||||||
|
preg_match('/<result>(.*?)<\/result>/s', $soapResponse, $matches);
|
||||||
|
|
||||||
|
if (!isset($matches[1])) {
|
||||||
|
$logger->error('No se encontró tag <result> en la respuesta SOAP', ['response' => $soapResponse]);
|
||||||
|
throw new PadronEstatalException("No se pudo extraer el resultado del padrón estatal");
|
||||||
|
}
|
||||||
|
|
||||||
|
$jsonContent = trim($matches[1]);
|
||||||
|
|
||||||
|
// Decodificar el JSON
|
||||||
|
$result = json_decode($jsonContent, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$logger->error('Error al decodificar JSON del padrón estatal', [
|
||||||
|
'json_error' => json_last_error_msg(),
|
||||||
|
'content' => $jsonContent,
|
||||||
|
]);
|
||||||
|
throw new PadronEstatalException("Error al decodificar JSON del padrón estatal: " . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
// La respuesta es un array con un objeto que tiene error y datos
|
||||||
|
if (!isset($result[0])) {
|
||||||
|
$logger->error('Formato de respuesta inesperado (sin índice 0)', ['result' => $result]);
|
||||||
|
throw new PadronEstatalException("Formato de respuesta inesperado del padrón estatal");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $result[0];
|
||||||
|
|
||||||
|
// Verificar si hay error
|
||||||
|
if ($data['error'] !== 0) {
|
||||||
|
$logger->warning('El padrón estatal devolvió un código de error', [
|
||||||
|
'error_code' => $data['error'],
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
throw new PadronEstatalException("Error en consulta al padrón estatal: código {$data['error']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si hay datos
|
||||||
|
if (!isset($data['datos'][0])) {
|
||||||
|
$logger->warning('El padrón estatal no devolvió datos del vehículo', ['data' => $data]);
|
||||||
|
throw new PadronEstatalException("No se encontraron datos del vehículo en el padrón estatal");
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger->info('Consulta al padrón estatal exitosa');
|
||||||
|
|
||||||
|
return $data['datos'][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrae los datos del vehículo del resultado
|
||||||
|
*/
|
||||||
|
public function extraerDatosVehiculo(array $datos): array
|
||||||
|
{
|
||||||
|
$fechaexpedicion = $datos['fechaexp'] ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'placa' => $datos['placa'] ?? null,
|
||||||
|
'niv' => $datos['niv'] ?? null,
|
||||||
|
'marca' => $datos['marca'] ?? null,
|
||||||
|
'linea' => $datos['submarca'] ?? null,
|
||||||
|
'sublinea' => $datos['version'] ?? null,
|
||||||
|
'modelo' => $datos['modelo'] ?? null,
|
||||||
|
'color' => $datos['color'] ?? null,
|
||||||
|
'numero_motor' => $datos['motor'] ?? null,
|
||||||
|
'clase_veh' => $datos['clase_veh'] ?? null,
|
||||||
|
'tipo_servicio' => $datos['tipo_uso'] ?? null,
|
||||||
|
'rfv' => $datos['rfv'] ?? null,
|
||||||
|
'ofcexpedicion' => $datos['ofcexp'] ?? null,
|
||||||
|
'fechaexpedicion' => $fechaexpedicion,
|
||||||
|
'tipo_veh' => $datos['tipo_veh'] ?? null,
|
||||||
|
'numptas' => $datos['numptas'] ?? null,
|
||||||
|
'observac' => $datos['observac'] ?? null,
|
||||||
|
'cve_vehi' => $datos['cve_vehi'] ?? null,
|
||||||
|
'nrpv' => $datos['nrpv'] ?? null,
|
||||||
|
'tipo_mov' => $datos['tipo_mov'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrae los datos del propietario del resultado
|
||||||
|
*/
|
||||||
|
public function extraerDatosPropietario(array $datos): array
|
||||||
|
{
|
||||||
|
// Construir dirección completa
|
||||||
|
$addressParts = array_filter([
|
||||||
|
$datos['callep'] ?? null,
|
||||||
|
isset($datos['num_ext']) && $datos['num_ext'] ? "Num {$datos['num_ext']}" : null,
|
||||||
|
isset($datos['num_int']) && $datos['num_int'] ? "Int {$datos['num_int']}" : null,
|
||||||
|
$datos['colonia'] ?? null,
|
||||||
|
isset($datos['cp']) && $datos['cp'] ? "CP {$datos['cp']}" : null,
|
||||||
|
isset($datos['munic']) && $datos['munic'] ? "Mun {$datos['munic']}" : null,
|
||||||
|
isset($datos['ent_fed']) && $datos['ent_fed'] ? "Edo {$datos['ent_fed']}" : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$address = implode(', ', $addressParts);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $datos['nombre'] ?? null,
|
||||||
|
'paternal' => $datos['ap_paterno'] ?? null,
|
||||||
|
'maternal' => $datos['ap_materno'] ?? null,
|
||||||
|
'rfc' => $datos['rfc'] ?? null,
|
||||||
|
'curp' => $datos['curp'] ?? null,
|
||||||
|
'address' => $address,
|
||||||
|
'tipopers' => $datos['tipopers'] ?? null,
|
||||||
|
'pasaporte' => $datos['pasaporte'] ?? null,
|
||||||
|
'licencia' => $datos['licencia'] ?? null,
|
||||||
|
'ent_fed' => $datos['ent_fed'] ?? null,
|
||||||
|
'munic' => $datos['munic'] ?? null,
|
||||||
|
'callep' => $datos['callep'] ?? null,
|
||||||
|
'num_ext' => $datos['num_ext'] ?? null,
|
||||||
|
'num_int' => $datos['num_int'] ?? null,
|
||||||
|
'colonia' => $datos['colonia'] ?? null,
|
||||||
|
'cp' => $datos['cp'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
855
app/Services/RepuveService.php
Normal file
855
app/Services/RepuveService.php
Normal file
@ -0,0 +1,855 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use App\Models\Error;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Helpers\EncryptionHelper;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class RepuveService
|
||||||
|
{
|
||||||
|
private string $baseUrl;
|
||||||
|
private string $roboEndpoint;
|
||||||
|
private string $inscripcionEndpoint;
|
||||||
|
private ?string $username = null;
|
||||||
|
private ?string $password = null;
|
||||||
|
private bool $credentialsLoaded = false;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->baseUrl = config('services.repuve_federal.base_url');
|
||||||
|
$this->roboEndpoint = config('services.repuve_federal.robo_endpoint');
|
||||||
|
$this->inscripcionEndpoint = config('services.repuve_federal.inscripcion_endpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asegurar que las credenciales estén cargadas (lazy loading)
|
||||||
|
*/
|
||||||
|
private function asegurarCargaCredenciales(): void
|
||||||
|
{
|
||||||
|
if ($this->credentialsLoaded) {
|
||||||
|
return; // Ya están cargadas
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadCredentials();
|
||||||
|
$this->credentialsLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargar credenciales desde BD
|
||||||
|
*/
|
||||||
|
private function loadCredentials(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Obtener credenciales encriptadas desde BD
|
||||||
|
$encryptedCredentials = Setting::value('repuve_federal_credentials');
|
||||||
|
|
||||||
|
if (!$encryptedCredentials) {
|
||||||
|
throw new Exception('Credenciales REPUVE no configuradas en el sistema');
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = EncryptionHelper::decryptData($encryptedCredentials);
|
||||||
|
|
||||||
|
if (!$credentials || !isset($credentials['username'], $credentials['password'])) {
|
||||||
|
throw new Exception('Error al desencriptar credenciales REPUVE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->username = $credentials['username'];
|
||||||
|
$this->password = $credentials['password'];
|
||||||
|
|
||||||
|
Log::channel('repuve_nacional')->info('RepuveService: Credenciales cargadas correctamente desde BD');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::channel('repuve_nacional')->error('RepuveService: Error al cargar credenciales', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new Exception('No se pudieron cargar las credenciales REPUVE: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta una solicitud cURL con logging completo de conexión, datos enviados,
|
||||||
|
* tiempo de respuesta y respuesta recibida.
|
||||||
|
*
|
||||||
|
* @return array{response: string|false, http_code: int, curl_error: string, elapsed_ms: float}
|
||||||
|
*/
|
||||||
|
private function ejecutarSolicitudSoap(\CurlHandle $ch, string $operacion, string $url, string $soapBody, array $contexto = []): array
|
||||||
|
{
|
||||||
|
Log::channel('repuve_nacional')->info("REPUVE Nacional [{$operacion}]: Enviando solicitud al servidor", array_merge([
|
||||||
|
'url' => $url,
|
||||||
|
'soap_body' => $soapBody,
|
||||||
|
], $contexto));
|
||||||
|
|
||||||
|
$inicio = microtime(true);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$elapsedMs = round((microtime(true) - $inicio) * 1000, 2);
|
||||||
|
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
Log::channel('repuve_nacional')->error("REPUVE Nacional [{$operacion}]: Sin conexión con el servidor", array_merge([
|
||||||
|
'url' => $url,
|
||||||
|
'curl_error' => $curlError,
|
||||||
|
'lapso_ms' => $elapsedMs,
|
||||||
|
], $contexto));
|
||||||
|
} else {
|
||||||
|
Log::channel('repuve_nacional')->info("REPUVE Nacional [{$operacion}]: Respuesta recibida", array_merge([
|
||||||
|
'url' => $url,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'lapso_ms' => $elapsedMs,
|
||||||
|
'response_length' => strlen($response ?: ''),
|
||||||
|
'response' => $response,
|
||||||
|
], $contexto));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'response' => $response,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'curl_error' => $curlError,
|
||||||
|
'lapso_ms' => $elapsedMs,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consultarPadron(string $niv)
|
||||||
|
{
|
||||||
|
$this->asegurarCargaCredenciales();
|
||||||
|
|
||||||
|
$url = $this->baseUrl . $this->roboEndpoint;
|
||||||
|
|
||||||
|
// 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos
|
||||||
|
$campos = array_fill(0, 8, '');
|
||||||
|
$campos[0] = $niv;
|
||||||
|
$arg2 = implode('|', $campos);
|
||||||
|
|
||||||
|
$soapBody = <<<XML
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<wsdl:doConsPadron>
|
||||||
|
<arg0>{$this->username}</arg0>
|
||||||
|
<arg1>{$this->password}</arg1>
|
||||||
|
<arg2>{$arg2}</arg2>
|
||||||
|
</wsdl:doConsPadron>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=utf-8',
|
||||||
|
'SOAPAction: "doConsPadron"',
|
||||||
|
'Content-Length: ' . strlen($soapBody),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->ejecutarSolicitudSoap($ch, 'doConsPadron', $url, $soapBody, ['niv' => $niv]);
|
||||||
|
$response = $result['response'];
|
||||||
|
$httpCode = $result['http_code'];
|
||||||
|
$error = $result['curl_error'];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
throw new Exception("Error en la petición SOAP: {$error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
throw new Exception("Error al consultar REPUVE: Código HTTP {$httpCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parseVehicleResponse($response, $niv);
|
||||||
|
} finally {
|
||||||
|
unset($ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function parseVehicleResponse(string $soapResponse, string $niv)
|
||||||
|
{
|
||||||
|
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||||
|
|
||||||
|
if (!isset($matches[1])) {
|
||||||
|
$errorFromDb = Error::where('code', '108')->first();
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => '108',
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'niv' => $niv,
|
||||||
|
'repuve_response' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$contenido = trim($matches[1]);
|
||||||
|
|
||||||
|
// Verificar si hay error de REPUVE Nacional (cualquier formato con ERR o ERROR)
|
||||||
|
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||||
|
$errorCode = $errorMatch[2];
|
||||||
|
|
||||||
|
// Buscar el error completo en la base de datos
|
||||||
|
$errorFromDb = Error::where('code', $errorCode)->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? "Error código {$errorCode} - no catalogado",
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'niv' => $niv,
|
||||||
|
'repuve_response' => $contenido,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si empieza con OK:, parsear los datos
|
||||||
|
if (str_starts_with($contenido, 'OK:')) {
|
||||||
|
$datos = str_replace('OK:', '', $contenido);
|
||||||
|
$valores = explode('|', $datos);
|
||||||
|
|
||||||
|
$campos = [
|
||||||
|
'marca',
|
||||||
|
'submarca',
|
||||||
|
'tipo_vehiculo',
|
||||||
|
'fecha_expedicion',
|
||||||
|
'oficina',
|
||||||
|
'niv',
|
||||||
|
'placa',
|
||||||
|
'motor',
|
||||||
|
'modelo',
|
||||||
|
'color',
|
||||||
|
'version',
|
||||||
|
'entidad',
|
||||||
|
'marca_padron',
|
||||||
|
'submarca_padron',
|
||||||
|
'tipo_uso_padron',
|
||||||
|
'tipo_vehiculo_padron',
|
||||||
|
'estatus_registro',
|
||||||
|
'aduana',
|
||||||
|
'nombre_aduana',
|
||||||
|
'patente',
|
||||||
|
'pedimento',
|
||||||
|
'fecha_pedimento',
|
||||||
|
'clave_importador',
|
||||||
|
'folio_CI',
|
||||||
|
'identificador_CI',
|
||||||
|
'observaciones',
|
||||||
|
];
|
||||||
|
|
||||||
|
$jsonResponse = [];
|
||||||
|
foreach ($campos as $i => $campo) {
|
||||||
|
$jsonResponse[$campo] = $valores[$i] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => false,
|
||||||
|
'error_code' => null,
|
||||||
|
'error_message' => null,
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'niv' => $niv,
|
||||||
|
'repuve_response' => $jsonResponse,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorFromDb = Error::where('code', '108')->first();
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => '108',
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'niv' => $niv,
|
||||||
|
'repuve_response' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verificarRobo(?string $niv = null, ?string $placa = null): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->asegurarCargaCredenciales();
|
||||||
|
|
||||||
|
if (empty($niv) && empty($placa)) {
|
||||||
|
Log::channel('repuve_nacional')->warning('REPUVE verificarRobo: No se proporcionó NIV ni PLACA');
|
||||||
|
return [
|
||||||
|
'es_robado' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Debe proporcionar al menos NIV o PLACA para verificar robo',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $this->baseUrl . $this->roboEndpoint;
|
||||||
|
|
||||||
|
// 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos
|
||||||
|
// Ejemplo: LSGHD52H0ND032457||WNU700B|||||
|
||||||
|
$campos = array_fill(0, 8, '');
|
||||||
|
$campos[0] = $niv ?? '';
|
||||||
|
$campos[2] = $placa ?? '';
|
||||||
|
$arg2 = implode('|', $campos);
|
||||||
|
|
||||||
|
Log::channel('repuve_nacional')->info('REPUVE verificarRobo: Cadena construida', [
|
||||||
|
'niv' => $niv,
|
||||||
|
'placa' => $placa,
|
||||||
|
'arg2' => $arg2,
|
||||||
|
'total_pipes' => substr_count($arg2, '|'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$soapBody = <<<XML
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<wsdl:doConsRepRobo>
|
||||||
|
<arg0>{$this->username}</arg0>
|
||||||
|
<arg1>{$this->password}</arg1>
|
||||||
|
<arg2>{$arg2}</arg2>
|
||||||
|
</wsdl:doConsRepRobo>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=utf-8',
|
||||||
|
'SOAPAction: "doConsRepRobo"',
|
||||||
|
'Content-Length: ' . strlen($soapBody),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->ejecutarSolicitudSoap($ch, 'doConsRepRobo', $url, $soapBody, ['niv' => $niv, 'placa' => $placa]);
|
||||||
|
$response = $result['response'];
|
||||||
|
$httpCode = $result['http_code'];
|
||||||
|
$error = $result['curl_error'];
|
||||||
|
|
||||||
|
// Si hay error de conexión, retornar error
|
||||||
|
if ($error) {
|
||||||
|
return [
|
||||||
|
'is_robado' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Error de conexión con el servicio REPUVE',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si hay error HTTP, retornar error
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
return [
|
||||||
|
'is_robado' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => "Error HTTP {$httpCode} del servicio REPUVE",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsear respuesta
|
||||||
|
$valorBuscado = $niv ?: $placa ?: 'N/A';
|
||||||
|
$resultado = $this->parseRoboResponse($response, $valorBuscado);
|
||||||
|
|
||||||
|
// Si hubo error al parsear, loguear pero retornar el resultado completo
|
||||||
|
if ($resultado['has_error'] ?? false) {
|
||||||
|
Log::channel('repuve_nacional')->warning('REPUVE verificarRobo: Error al parsear respuesta', [
|
||||||
|
'niv' => $niv,
|
||||||
|
'placa' => $placa,
|
||||||
|
'error' => $resultado['error_message'] ?? 'Desconocido',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retornar el array completo con toda la información
|
||||||
|
return $resultado;
|
||||||
|
} finally {
|
||||||
|
unset($ch);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::channel('repuve_nacional')->error('REPUVE verificarRobo: Excepción capturada', [
|
||||||
|
'niv' => $niv,
|
||||||
|
'placa' => $placa,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
'es_robado' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Excepción capturada: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consultarVehiculo(?string $niv = null, ?string $placa = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->asegurarCargaCredenciales();
|
||||||
|
|
||||||
|
$url = $this->baseUrl . '/jaxws-consultarpv/ConsultaRpv';
|
||||||
|
|
||||||
|
// 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos
|
||||||
|
// Ejemplo: LSGHD52H0ND032457||WNU700B|||||
|
||||||
|
$campos = array_fill(0, 8, '');
|
||||||
|
$campos[0] = $niv ?? '';
|
||||||
|
$campos[2] = $placa ?? '';
|
||||||
|
$arg2 = implode('|', $campos);
|
||||||
|
|
||||||
|
$soapBody = <<<XML
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<wsdl:doConsRPV>
|
||||||
|
<arg0>{$this->username}</arg0>
|
||||||
|
<arg1>{$this->password}</arg1>
|
||||||
|
<arg2>{$arg2}</arg2>
|
||||||
|
</wsdl:doConsRPV>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=utf-8',
|
||||||
|
'SOAPAction: "doConsRPV"',
|
||||||
|
'Content-Length: ' . strlen($soapBody),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->ejecutarSolicitudSoap($ch, 'doConsRPV', $url, $soapBody, ['niv' => $niv, 'placa' => $placa]);
|
||||||
|
$response = $result['response'];
|
||||||
|
$httpCode = $result['http_code'];
|
||||||
|
$error = $result['curl_error'];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Error de conexión con el servicio REPUVE',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => "Error HTTP {$httpCode} del servicio REPUVE",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsear respuesta
|
||||||
|
$resultado = $this->parseConsultarVehiculoResponse($response);
|
||||||
|
|
||||||
|
if ($resultado['has_error'] ?? false) {
|
||||||
|
Log::channel('repuve_nacional')->warning('REPUVE consultarVehiculo: Error al parsear', [
|
||||||
|
'niv' => $niv,
|
||||||
|
'placa' => $placa,
|
||||||
|
'error' => $resultado['error_message'] ?? 'Desconocido',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resultado;
|
||||||
|
} finally {
|
||||||
|
unset($ch);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::channel('repuve_nacional')->error('REPUVE consultarVehiculo: Excepción', [
|
||||||
|
'niv' => $niv,
|
||||||
|
'placa' => $placa,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Excepción: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inscribirVehiculo(array $datos)
|
||||||
|
{
|
||||||
|
$this->asegurarCargaCredenciales();
|
||||||
|
|
||||||
|
$url = $this->baseUrl . $this->inscripcionEndpoint;
|
||||||
|
|
||||||
|
$arg2 = implode('|', [
|
||||||
|
$datos['ent_fed'] ?? '', // 1. Entidad federativa
|
||||||
|
$datos['ofcexp'] ?? '', // 2. Oficina expedición
|
||||||
|
$datos['fechaexp'] ?? '', // 3. Fecha expedición
|
||||||
|
$datos['placa'] ?? '', // 4. Placa
|
||||||
|
$datos['tarjetacir'] ?? '', // 5. Tarjeta circulación
|
||||||
|
$datos['marca'] ?? '', // 6. Marca
|
||||||
|
$datos['submarca'] ?? '', // 7. Submarca
|
||||||
|
$datos['version'] ?? '', // 8. Versión
|
||||||
|
$datos['clase_veh'] ?? '', // 9. Clase vehículo
|
||||||
|
$datos['tipo_veh'] ?? '', // 10. Tipo vehículo
|
||||||
|
$datos['tipo_uso'] ?? '', // 11. Tipo uso
|
||||||
|
$datos['modelo'] ?? '', // 12. Modelo (año)
|
||||||
|
$datos['color'] ?? '', // 13. Color
|
||||||
|
$datos['motor'] ?? '', // 14. Número motor
|
||||||
|
$datos['niv'] ?? '', // 15. NIV
|
||||||
|
$datos['rfv'] ?? '', // 16. RFV
|
||||||
|
$datos['numptas'] ?? '', // 17. Número puertas
|
||||||
|
$datos['observac'] ?? '', // 18. Observaciones
|
||||||
|
$datos['tipopers'] ?? '', // 19. Tipo persona
|
||||||
|
$datos['curp'] ?? '', // 20. CURP
|
||||||
|
$datos['rfc'] ?? '', // 21. RFC
|
||||||
|
$datos['pasaporte'] ?? '', // 22. Pasaporte
|
||||||
|
$datos['licencia'] ?? '', // 23. Licencia
|
||||||
|
$datos['nombre'] ?? '', // 24. Nombre
|
||||||
|
$datos['ap_paterno'] ?? '', // 25. Apellido paterno
|
||||||
|
$datos['ap_materno'] ?? '', // 26. Apellido materno
|
||||||
|
$datos['ent_fed'] ?? '', // 27. Entidad federativa propietario
|
||||||
|
$datos['munic'] ?? '', // 28. Municipio
|
||||||
|
$datos['callep'] ?? '', // 29. Calle principal
|
||||||
|
$datos['num_ext'] ?? '', // 30. Número exterior
|
||||||
|
$datos['num_int'] ?? '', // 31. Número interior
|
||||||
|
$datos['colonia'] ?? '', // 32. Colonia
|
||||||
|
$datos['cp'] ?? '', // 33. Código postal
|
||||||
|
$datos['cve_vehi'] ?? '', // 34. Clave vehículo
|
||||||
|
$datos['nrpv'] ?? '', // 35. NRPV
|
||||||
|
$datos['fe_act'] ?? '', // 36. Fecha actualización
|
||||||
|
$datos['tipo_mov'] ?? '', // 37. Tipo movimiento
|
||||||
|
$datos['folio_CI'] ?? '', // 38. Folio constancia de inscripción
|
||||||
|
$datos['identificador_CI'] ?? '', // 39. Identificador constancia de inscripción
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Construir el cuerpo SOAP
|
||||||
|
$soapBody = <<<XML
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://inscripcion.org/wsdl">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<wsdl:inscribe>
|
||||||
|
<arg0>{$this->username}</arg0>
|
||||||
|
<arg1>{$this->password}</arg1>
|
||||||
|
<arg2>{$arg2}</arg2>
|
||||||
|
</wsdl:inscribe>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
// Configurar cURL
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: text/xml; charset=utf-8',
|
||||||
|
'SOAPAction: "inscribe"',
|
||||||
|
'Content-Length: ' . strlen($soapBody),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->ejecutarSolicitudSoap($ch, 'inscribe', $url, $soapBody, [
|
||||||
|
'niv' => $datos['niv'] ?? null,
|
||||||
|
'placa' => $datos['placa'] ?? null,
|
||||||
|
'nrpv' => $datos['nrpv'] ?? null,
|
||||||
|
]);
|
||||||
|
$response = $result['response'];
|
||||||
|
$httpCode = $result['http_code'];
|
||||||
|
$curlError = $result['curl_error'];
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
$errorFromDb = Error::where('code', '103')->first();
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => '103',
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? "Error de conexión: {$curlError}",
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'raw_response' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
$errorFromDb = Error::where('code', '-1')->first();
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => '-1',
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? "Error interno HTTP {$httpCode}",
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'raw_response' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsear la respuesta
|
||||||
|
return $this->parsearRespuestaInscripcion($response);
|
||||||
|
} finally {
|
||||||
|
unset($ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea la respuesta
|
||||||
|
*/
|
||||||
|
private function parsearRespuestaInscripcion(string $soapResponse)
|
||||||
|
{
|
||||||
|
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||||
|
|
||||||
|
if (!isset($matches[1])) {
|
||||||
|
$errorFromDb = Error::where('code', '108')->first();
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => '108',
|
||||||
|
'error_name' => $errorFromDb?->name,
|
||||||
|
'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta',
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'raw_response' => $soapResponse,
|
||||||
|
'repuve_response' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$contenido = trim($matches[1]);
|
||||||
|
|
||||||
|
// Buscar patrones de error: ERR:, ERROR:, err:, error:
|
||||||
|
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||||
|
$errorCode = $errorMatch[2];
|
||||||
|
|
||||||
|
// Buscar el error completo en la base de datos
|
||||||
|
$errorFromDb = Error::where('code', $errorCode)->first();
|
||||||
|
|
||||||
|
if ($errorFromDb) {
|
||||||
|
// Retornar nombre y descripción de la BD
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_name' => $errorFromDb->name,
|
||||||
|
'error_message' => $errorFromDb->description,
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'raw_response' => $soapResponse,
|
||||||
|
'repuve_response' => $contenido,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no existe en BD, retornar el código sin descripción
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_name' => null,
|
||||||
|
'error_message' => "Error código {$errorCode} - no catalogado",
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'raw_response' => $soapResponse,
|
||||||
|
'repuve_response' => $contenido,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si empieza con OK: es éxito
|
||||||
|
if (preg_match('/^OK:/i', $contenido)) {
|
||||||
|
$datos = preg_replace('/^OK:/i', '', $contenido);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => false,
|
||||||
|
'error_code' => null,
|
||||||
|
'error_message' => null,
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'raw_response' => $soapResponse,
|
||||||
|
'repuve_response' => [
|
||||||
|
'status' => 'OK',
|
||||||
|
'data' => $datos,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no hay ERR/ERROR y no es OK, asumir que es respuesta exitosa
|
||||||
|
return [
|
||||||
|
'has_error' => false,
|
||||||
|
'error_code' => null,
|
||||||
|
'error_name' => null,
|
||||||
|
'error_message' => null,
|
||||||
|
'timestamp' => now()->toDateTimeString(),
|
||||||
|
'raw_response' => $soapResponse,
|
||||||
|
'repuve_response' => [
|
||||||
|
'status' => 'OK',
|
||||||
|
'data' => $contenido,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseRoboResponse(string $soapResponse, string $valor): array
|
||||||
|
{
|
||||||
|
// Extraer contenido del tag <return>
|
||||||
|
preg_match('/<return>(.*?)<\/return>/s', $soapResponse, $matches);
|
||||||
|
|
||||||
|
if (!isset($matches[1])) {
|
||||||
|
Log::channel('repuve_nacional')->error('REPUVE parseRoboResponse: No se encontró tag <return>', [
|
||||||
|
'soap_response' => substr($soapResponse, 0, 500),
|
||||||
|
'valor' => $valor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'is_robado' => false,
|
||||||
|
'error_message' => 'Respuesta SOAP inválida',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$contenido = trim($matches[1]);
|
||||||
|
|
||||||
|
// Verificar si hay error de REPUVE Nacional (ERR: o ERROR:)
|
||||||
|
if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) {
|
||||||
|
$errorCode = $errorMatch[2];
|
||||||
|
|
||||||
|
Log::channel('repuve_nacional')->warning('REPUVE parseRoboResponse: Servicio retornó error', [
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'contenido' => $contenido,
|
||||||
|
'valor' => $valor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'es_robado' => false,
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_message' => "Error REPUVE código {$errorCode}",
|
||||||
|
'raw_response' => $contenido,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si empieza con OK:, parsear los datos de robo
|
||||||
|
if (str_starts_with($contenido, 'OK:')) {
|
||||||
|
$datos = str_replace('OK:', '', $contenido);
|
||||||
|
$valores = explode('|', $datos);
|
||||||
|
|
||||||
|
// 1 = robado, 0 = no robado
|
||||||
|
$indicador = $valores[0] ?? '0';
|
||||||
|
$isRobado = ($indicador === '1');
|
||||||
|
|
||||||
|
$roboData = [
|
||||||
|
'has_error' => false,
|
||||||
|
'es_robado' => $isRobado,
|
||||||
|
'indicador' => $indicador,
|
||||||
|
'fecha_robo' => $valores[1] ?? null,
|
||||||
|
'placa' => $valores[2] ?? null,
|
||||||
|
'niv' => $valores[3] ?? null,
|
||||||
|
'autoridad' => $valores[4] ?? null,
|
||||||
|
'acta' => $valores[5] ?? null,
|
||||||
|
'denunciante' => $valores[6] ?? null,
|
||||||
|
'fecha_acta' => $valores[7] ?? null,
|
||||||
|
'raw_response' => $contenido,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Log importante si está robado
|
||||||
|
if ($isRobado) {
|
||||||
|
Log::channel('repuve_nacional')->warning('REPUVE: Vehículo reportado como ROBADO', [
|
||||||
|
'valor_buscado' => $valor,
|
||||||
|
'niv' => $roboData['niv'],
|
||||||
|
'placa' => $roboData['placa'],
|
||||||
|
'autoridad' => $roboData['autoridad'],
|
||||||
|
'acta' => $roboData['acta'],
|
||||||
|
'denunciante' => $roboData['denunciante'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Log::channel('repuve_nacional')->info('REPUVE: Vehículo NO reportado como robado', [
|
||||||
|
'valor_buscado' => $valor,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $roboData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no tiene formato reconocido
|
||||||
|
Log::channel('repuve_nacional')->error('REPUVE parseRoboResponse: Formato desconocido', [
|
||||||
|
'contenido' => $contenido,
|
||||||
|
'valor' => $valor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'has_error' => true,
|
||||||
|
'is_robado' => false,
|
||||||
|
'error_message' => 'Formato de respuesta no reconocido',
|
||||||
|
'raw_response' => $contenido,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseConsultarVehiculoResponse(string $xmlResponse): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$xml = simplexml_load_string($xmlResponse);
|
||||||
|
|
||||||
|
if ($xml === false) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Error al parsear XML',
|
||||||
|
'raw_response' => $xmlResponse,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml->registerXPathNamespace('ns2', 'http://consultaRpv.org/wsdl');
|
||||||
|
$return = $xml->xpath('//ns2:doConsRPVResponse/return');
|
||||||
|
|
||||||
|
if (empty($return)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'No se encontró elemento return en la respuesta',
|
||||||
|
'raw_response' => $xmlResponse,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$contenido = trim((string)$return[0]);
|
||||||
|
|
||||||
|
// Verificar si la respuesta es OK
|
||||||
|
if (!str_starts_with($contenido, 'OK:')) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => $contenido,
|
||||||
|
'raw_response' => $xmlResponse,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover "OK:" del inicio
|
||||||
|
$data = substr($contenido, 3);
|
||||||
|
|
||||||
|
// Separar por |
|
||||||
|
$campos = explode('|', $data);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'has_error' => false,
|
||||||
|
'entidad_federativa' => $campos[0] ?? null,
|
||||||
|
'oficina' => $campos[1] ?? null,
|
||||||
|
'folio_tarjeta' => $campos[2] ?? null,
|
||||||
|
'niv' => $campos[3] ?? null,
|
||||||
|
'fecha_expedicion' => $campos[4] ?? null,
|
||||||
|
'hora_expedicion' => $campos[5] ?? null,
|
||||||
|
'procedencia' => $campos[6] ?? null,
|
||||||
|
'origen' => $campos[7] ?? null,
|
||||||
|
'clave_vehicular' => $campos[8] ?? null,
|
||||||
|
'fecha_emplacado' => $campos[9] ?? null,
|
||||||
|
'municipio' => $campos[10] ?? null,
|
||||||
|
'serie' => $campos[11] ?? null,
|
||||||
|
'placa' => $campos[12] ?? null,
|
||||||
|
'tipo_vehiculo' => $campos[13] ?? null,
|
||||||
|
'modelo' => $campos[14] ?? null,
|
||||||
|
'color' => $campos[15] ?? null,
|
||||||
|
'version' => $campos[16] ?? null,
|
||||||
|
'entidad_placas' => $campos[17] ?? null,
|
||||||
|
'marca' => $campos[18] ?? null,
|
||||||
|
'linea' => $campos[19] ?? null,
|
||||||
|
'uso' => $campos[20] ?? null,
|
||||||
|
'clase' => $campos[21] ?? null,
|
||||||
|
'estatus' => $campos[22] ?? null,
|
||||||
|
'folio_CI' => $campos[29] ?? null,
|
||||||
|
'identificador_CI' => $campos[30] ?? null,
|
||||||
|
'observaciones' => $campos[31] ?? null,
|
||||||
|
'raw_response' => $contenido,
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'has_error' => true,
|
||||||
|
'error_message' => 'Excepción al parsear: ' . $e->getMessage(),
|
||||||
|
'raw_response' => $xmlResponse,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@
|
|||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
use Notsoweb\LaravelCore\Http\APIException;
|
use Notsoweb\LaravelCore\Http\APIException;
|
||||||
|
use Spatie\Permission\Exceptions\UnauthorizedException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
@ -44,6 +46,20 @@
|
|||||||
return ApiResponse::SERVICE_UNAVAILABLE->response();
|
return ApiResponse::SERVICE_UNAVAILABLE->response();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$exceptions->render(function (UnauthorizedException $e, Request $request) {
|
||||||
|
if ($request->is('api/*')) {
|
||||||
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$exceptions->render(function (AccessDeniedHttpException $e, Request $request) {
|
||||||
|
if ($request->is('api/*')) {
|
||||||
|
return ApiResponse::UNPROCESSABLE_CONTENT->response([
|
||||||
|
'message' => __($e->getMessage())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
$exceptions->render(APIException::notFound(...));
|
$exceptions->render(APIException::notFound(...));
|
||||||
$exceptions->render(APIException::unauthorized(...));
|
$exceptions->render(APIException::unauthorized(...));
|
||||||
$exceptions->render(APIException::unprocessableContent(...));
|
$exceptions->render(APIException::unprocessableContent(...));
|
||||||
|
|||||||
@ -7,16 +7,24 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
|
"barryvdh/laravel-dompdf": "*",
|
||||||
|
"codedge/laravel-fpdf": "*",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/passport": "^12.4",
|
"laravel/passport": "^12.4",
|
||||||
"laravel/pulse": "^1.4",
|
"laravel/pulse": "^1.4",
|
||||||
"laravel/reverb": "^1.4",
|
"laravel/reverb": "^1.4",
|
||||||
"laravel/tinker": "^2.10",
|
"laravel/tinker": "^2.10",
|
||||||
|
"milon/barcode": "^12.0",
|
||||||
"notsoweb/laravel-core": "dev-main",
|
"notsoweb/laravel-core": "dev-main",
|
||||||
|
"phpoffice/phpspreadsheet": "*",
|
||||||
|
"phpoffice/phpword": "^1.4",
|
||||||
|
"setasign/fpdf": "^1.8",
|
||||||
"spatie/laravel-permission": "^6.16",
|
"spatie/laravel-permission": "^6.16",
|
||||||
|
"spatie/simple-excel": "^3.8",
|
||||||
"tightenco/ziggy": "^2.5"
|
"tightenco/ziggy": "^2.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"barryvdh/laravel-ide-helper": "^3.7",
|
||||||
"fakerphp/faker": "^1.24",
|
"fakerphp/faker": "^1.24",
|
||||||
"laravel/pail": "^1.2",
|
"laravel/pail": "^1.2",
|
||||||
"laravel/pint": "^1.21",
|
"laravel/pint": "^1.21",
|
||||||
@ -78,6 +86,10 @@
|
|||||||
"@php artisan migrate:fresh --seed",
|
"@php artisan migrate:fresh --seed",
|
||||||
"@php artisan passport:client --personal --name=Holos"
|
"@php artisan passport:client --personal --name=Holos"
|
||||||
],
|
],
|
||||||
|
"db:update": [
|
||||||
|
"@php artisan migrate",
|
||||||
|
"@php artisan migrate --path=database/migrations/data"
|
||||||
|
],
|
||||||
"jobs:start": [
|
"jobs:start": [
|
||||||
"pm2 start \"php artisan schedule:work\" --name holos-schedules",
|
"pm2 start \"php artisan schedule:work\" --name holos-schedules",
|
||||||
"pm2 start \"php artisan queue:work\" --name holos-queue"
|
"pm2 start \"php artisan queue:work\" --name holos-queue"
|
||||||
@ -110,6 +122,18 @@
|
|||||||
"services:status": [
|
"services:status": [
|
||||||
"composer run jobs:status",
|
"composer run jobs:status",
|
||||||
"composer run broadcast:status"
|
"composer run broadcast:status"
|
||||||
|
],
|
||||||
|
"docker:dev:up": [
|
||||||
|
"docker compose -f Docker/Dev/docker-compose.yml --env-file .env up -d --build"
|
||||||
|
],
|
||||||
|
"docker:dev:down": [
|
||||||
|
"docker compose -f Docker/Dev/docker-compose.yml --env-file .env down"
|
||||||
|
],
|
||||||
|
"docker:prod:up": [
|
||||||
|
"docker compose -f Docker/Prod/docker-compose.yml --env-file .env up -d --build"
|
||||||
|
],
|
||||||
|
"docker:prod:down": [
|
||||||
|
"docker compose -f Docker/Prod/docker-compose.yml --env-file .env down"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
|
|||||||
3651
composer.lock
generated
3651
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -52,7 +52,6 @@
|
|||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
'images' => [
|
'images' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/images'),
|
'root' => storage_path('app/images'),
|
||||||
|
|||||||
@ -135,5 +135,14 @@
|
|||||||
'driver' => 'single',
|
'driver' => 'single',
|
||||||
'path' => storage_path('logs/debug.log'),
|
'path' => storage_path('logs/debug.log'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'padron_estatal' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/padron-estatal.log'),
|
||||||
|
],
|
||||||
|
'repuve_nacional' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/repuve-nacional.log'),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -35,4 +35,16 @@
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'repuve_federal' => [
|
||||||
|
'base_url' => env('REPUVE_FED_BASE_URL'),
|
||||||
|
'robo_endpoint' => '/jaxws-consultarpv/ConsultaRpv',
|
||||||
|
'inscripcion_endpoint' => '/jaxrpc-inscripcion/Inscripcion?WSDLs=',
|
||||||
|
'username' => env('REPUVE_FED_USERNAME'),
|
||||||
|
'password' => env('REPUVE_FED_PASSWORD'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'padron_estatal' => [
|
||||||
|
'url' => env('REPUVE_EST_URL'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('modules', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('municipality');
|
||||||
|
$table->string('address');
|
||||||
|
$table->string('colony');
|
||||||
|
$table->string('cp')->nullable();
|
||||||
|
$table->decimal('longitude', 10, 8)->nullable();
|
||||||
|
$table->decimal('latitude', 10, 8)->nullable();
|
||||||
|
$table->boolean('status')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('modules');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('devices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('brand');
|
||||||
|
$table->string('serie')->unique();
|
||||||
|
$table->string('mac_address')->unique();
|
||||||
|
$table->boolean('status')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('devices');
|
||||||
|
}
|
||||||
|
};
|
||||||
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