This commit is contained in:
t0is 2025-01-14 22:09:35 +01:00
commit 59945ea99c
3351 changed files with 345633 additions and 0 deletions

7287
03-01-25-2025-01-03.log Normal file

File diff suppressed because it is too large Load Diff

142
docker-compose.yml Normal file
View File

@ -0,0 +1,142 @@
version: '3'
networks:
laravel:
# mysql:
# external: true
# name: mysql
traefik:
external: true
name: traefik
services:
nginx:
image: nginx:alpine-slim
restart: always
ports:
- '8080:80'
container_name: shipping-nginx
volumes:
# Please update below path as per your environment.
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
- laravel-redis
# - laravel-queue
# - laravel-cron
# - phpmyadmin
networks:
- laravel
- traefik
labels:
traefik.enable: true
# https
traefik.http.routers.shipping.entrypoints: https
traefik.http.routers.shipping.rule: Host(`shipping.dalkove-ovladace.cz`)
# traefik.http.routers.shipping.service: shipping
traefik.http.services.shipping.loadbalancer.server.port: 80
traefik.http.routers.shipping.tls.certresolver: mytlschallenge
php:
container_name: shipping-php
user: "www-data"
# restart: always
platform: linux/amd64
build:
# Please update below path as per your environment.
context: .
dockerfile: ./docker/php/Dockerfile
ports:
- '9001:9001'
volumes:
- ./src:/var/www/html
- ./src/.env:/var/www/html/.env
- ./docker/roadrunner.yaml:/var/www/html/.rr.yaml
- ./docker/policy.xml:/etc/ImageMagick-6/policy.xml
networks:
- laravel
- traefik
depends_on:
- laravel-redis
laravel-redis:
image: redis:alpine
container_name: laravel-redis
restart: unless-stopped
ports:
- 6380:6379
networks:
- laravel
- traefik
npm:
image: node:alpine
# container_name: npm
volumes:
- ./src:/var/www/html
- ./src/.env:/var/www/html/.env
ports:
- 3000:3000
- 3001:3001
working_dir: /var/www/html
profiles: [ "npm" ]
entrypoint: [ 'npm' ]
networks:
- laravel
- traefik
horizon:
container_name: shipping-horizon
platform: linux/arm64/v8
build:
context: .
dockerfile: ./docker/horizon.dockerfile
restart: unless-stopped
volumes:
- ./src:/var/www/html
- ./src/.env:/var/www/html/.env
- ./docker/supervisor/horizon.conf:/etc/supervisor/conf.d/horizon.conf
- ./docker/policy.xml:/etc/ImageMagick-6/policy.xml
depends_on:
- laravel-redis
networks:
- laravel
- traefik
pdf_manager:
container_name: shipping-pdf_manager
build:
context: .
dockerfile: ./docker/python/Dockerfile
ports:
- "5001:5000"
volumes:
- ./src/scripts/pdf_manage.py:/app/pdf_manage.py
- ./src/scripts/pdf_data:/app/pdfs
networks:
- laravel
- traefik
laravel-cron:
build:
context: ./docker
dockerfile: php.dockerfile
args:
- UID=${UID:-1001}
- GID=${GID:-1001}
- USER=${USER:-laravel}
container_name: laravel-cron
volumes:
- ./src:/var/www/html
- ./src/.env:/var/www/html/.env
working_dir: /var/www/html
entrypoint: [ 'php', '/var/www/html/artisan', 'schedule:work' ]
networks:
- laravel
- traefik

View File

@ -0,0 +1,19 @@
FROM composer:latest as composer
# environment arguments
ARG UID
ARG GID
ARG USER
ENV UID=${UID}
ENV GID=${GID}
ENV USER=${USER}
# Dialout group in alpine linux conflicts with MacOS staff group's gid, whis is 20. So we remove it.
RUN delgroup dialout
# Creating user and group
RUN addgroup -g ${GID} --system ${USER}
RUN adduser -G ${USER} --system -D -s /bin/sh -u ${UID} ${USER}
WORKDIR /var/www/html

25
docker/horizon.dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM t0is/laravel-roadrunner-base:latest-amd64
RUN apt update -y && apt upgrade -y
RUN apt-get install -y supervisor
RUN docker-php-ext-enable redis
WORKDIR /var/www/html
COPY src/. /var/www/html
COPY src/.env /var/www/html/.env
COPY docker/roadrunner.yaml /var/www/html/.rr.yaml
COPY docker/supervisor/horizon.conf /etc/supervisor/conf.d/horizon.conf
RUN composer install --optimize-autoloader --no-dev
RUN #php artisan optimize:clear && php artisan optimize
RUN chown -R www-data:www-data /var/www/html
RUN chmod -R 2755 /var/www/html/storage
RUN php artisan octane:install --server=roadrunner
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/horizon.conf"]

7
docker/init_db.sql Normal file
View File

@ -0,0 +1,7 @@
CREATE USER IF NOT EXISTS 'shipping'@'%' IDENTIFIED BY 'shipping';
CREATE DATABASE IF NOT EXISTS shipping CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON shipping.* TO shipping@'%';
GRANT ALL ON pps.* TO shipping@'%';
GRANT ALL ON vat_warehouse.* TO shipping@'%';
GRANT SELECT,DELETE,DROP ON mysql.general_log TO shipping@'%';
flush privileges;

26
docker/nginx.dockerfile Normal file
View File

@ -0,0 +1,26 @@
FROM nginx:stable-alpine
# environment arguments
ARG UID
ARG GID
ARG USER
ENV UID=${UID}
ENV GID=${GID}
ENV USER=${USER}
# Dialout group in alpine linux conflicts with MacOS staff group's gid, whis is 20. So we remove it.
RUN delgroup dialout
# Creating user and group
RUN addgroup -g ${GID} --system ${USER}
RUN adduser -G ${USER} --system -D -s /bin/sh -u ${UID} ${USER}
# Modify nginx configuration to use the new user's priviledges for starting it.
RUN sed -i "s/user nginx/user '${USER}'/g" /etc/nginx/nginx.conf
# Copies nginx configurations to override the default.
ADD ./nginx/*.conf /etc/nginx/conf.d/
# Make html directory
RUN mkdir -p /var/www/html

129
docker/nginx/default.conf Normal file
View File

@ -0,0 +1,129 @@
# Nginx default.conf
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
server_tokens off;
client_body_buffer_size 200;
client_max_body_size 200m;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream upstream_backend_php {
server shipping-php:9001;
}
server {
listen 80;
listen [::]:80;
server_name my_backend;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php index.html index.htm;
charset utf-8;
server_tokens off;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
# SEO trailing slash problem fix
# rewrite ^/(.*)/$ /$1 permanent; # remove trailing slash
# rewrite ^(.*[^/])$ $1/ permanent; # add a trailing slash
############################
# Reference: https://gist.github.com/Ellrion/4eb5df00173f0fb13a76
############################
location ~* \.(jpg|jpeg|png|gif|svg|webp|html|txt|json|ico|css|js)$ {
expires 1d;
add_header Cache-Control public;
access_log off;
try_files $uri $uri/ @octane;
}
location ~ /\.(?!well-known).* {
deny all;
}
# /etc/nginx/global/php-restrictions.conf
# Don't throw any errors for missing favicons and don't display them in the logs
location = /favicon.ico { log_not_found off; access_log off; try_files $uri $uri/ @octane;}
# Don't log missing robots or show them in the nginx logs
location = /robots.txt { allow all; log_not_found off; access_log off; try_files $uri $uri/ @octane;}
# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\. {
deny all;
}
# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
############################
# Customize
############################
location /index.php {
try_files /not_exists @octane;
}
location / {
if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) {
return 405;
}
try_files $uri $uri/ @octane;
}
location @octane {
set $suffix "";
if ($uri = /index.php) {
set $suffix ?$query_string;
}
proxy_http_version 1.1;
proxy_set_header Http_Host $http_host;
proxy_set_header Host $host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-CSRF-TOKEN $http_x_csrf_token;
proxy_set_header X-Real-IP $remote_addr;
# Add timeout settings
proxy_read_timeout 300s; # Increase read timeout
proxy_send_timeout 300s; # Increase send timeout
proxy_connect_timeout 300s; # Increase connect timeout
send_timeout 300s; # Increase overall send timeout
proxy_pass http://upstream_backend_php$suffix;
}
}

50
docker/php.dockerfile Normal file
View File

@ -0,0 +1,50 @@
FROM php:8.3-fpm
# Environment arguments
ARG UID
ARG GID
ARG USER
ENV UID=${UID}
ENV GID=${GID}
ENV USER=${USER}
# Creating user and group
RUN groupadd -g ${GID} ${USER} \
&& useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USER}
# Modify PHP-FPM configuration to use the new user's privileges.
RUN sed -i "s/user = www-data/user = '${USER}'/g" /usr/local/etc/php-fpm.d/www.conf \
&& sed -i "s/group = www-data/group = '${USER}'/g" /usr/local/etc/php-fpm.d/www.conf \
&& echo "php_admin_flag[log_errors] = on" >> /usr/local/etc/php-fpm.d/www.conf
# Update and install necessary packages
RUN apt-get update && apt-get install -y \
bash \
git \
curl \
libxml2-dev \
unzip \
python3 python3-venv python3-pip libmupdf-dev texlive-extra-utils \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
imagemagick \
&& docker-php-ext-install pdo pdo_mysql bcmath soap intl gd \
&& docker-php-ext-configure gd --with-freetype --with-jpeg
# Installing Redis extension
RUN mkdir -p /usr/src/php/ext/redis \
&& curl -fsSL https://github.com/phpredis/phpredis/archive/5.3.4.tar.gz | tar xvz -C /usr/src/php/ext/redis --strip 1 \
&& echo 'redis' >> /usr/src/php-available-exts \
&& docker-php-ext-install redis
# Set PHP configurations for memory, execution time, and file upload sizes
RUN echo "memory_limit = 1024M" > /usr/local/etc/php/conf.d/custom-php-memlimit.ini; \
echo "max_execution_time = 500" > /usr/local/etc/php/conf.d/custom-php-time.ini; \
echo "post_max_size = 100M" > /usr/local/etc/php/conf.d/custom-php-size.ini;
# Set the default command to run PHP-FPM
CMD ["php-fpm", "-y", "/usr/local/etc/php-fpm.conf", "-R"]

31
docker/php/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
FROM t0is/laravel-roadrunner-base:latest-amd64
RUN apt update -y && apt upgrade -y
WORKDIR /var/www/html
RUN docker-php-ext-enable redis
RUN python3 -m venv /var/www/html/shipping_env
# Activate the virtual environment and install pymupdf
RUN /var/www/html/shipping_env/bin/pip install --upgrade pip && \
/var/www/html/shipping_env/bin/pip install pymupdf
COPY src/. /var/www/html
COPY src/.env /var/www/html/.env
COPY docker/roadrunner.yaml /var/www/html/.rr.yaml
RUN #composer update --optimize-autoloader
RUN composer install --optimize-autoloader --no-dev
RUN #php artisan optimize:clear && php artisan optimize
RUN chown -R www-data:www-data /var/www/html
RUN chmod -R 2755 /var/www/html/storage
RUN php artisan octane:install --server=roadrunner
EXPOSE 9001
CMD ["php", "artisan", "octane:start", "--server=roadrunner", "--host=0.0.0.0", "--port=9001", "--log-level=debug", "--rr-config=/var/www/html/.rr.yaml"]

View File

@ -0,0 +1,59 @@
FROM php:8.3-cli
RUN apt update -y && apt upgrade -y
RUN apt install -y --no-install-recommends \
libfreetype-dev \
libjpeg62-turbo-dev \
libpng-dev \
imagemagick \
libmagickwand-dev \
git \
zip unzip \
openssl libssl-dev libcurl4-openssl-dev \
autoconf zlib1g-dev \
python3 python3-venv python3-pip ghostscript\
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd
RUN pecl install redis
RUN docker-php-ext-install opcache
RUN docker-php-ext-install pdo pdo_mysql
RUN docker-php-ext-install mysqli
RUN docker-php-ext-install sockets
RUN docker-php-ext-install soap
RUN docker-php-ext-install intl
# https://github.com/viest/php-ext-xlswriter
RUN pecl install xlswriter
# https://dev.to/kakisoft/php-docker-how-to-enable-pcntlprocess-control-extensions-1afk
RUN docker-php-ext-configure pcntl --enable-pcntl && docker-php-ext-install pcntl
RUN pecl install apcu
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN rm -rf /var/cache/apt/lists
# Please refer to the README.md for updating these 2 files.
#COPY src/composer.json /tmp
#RUN cd /tmp && composer install && composer update --optimize-autoloader
#RUN mv /tmp/vendor /var/www/html
#RUN cp /tmp/composer.* /var/www/html
#RUN composer clear-cache
RUN curl -L https://github.com/roadrunner-server/roadrunner/releases/download/v2024.3.0/roadrunner-2024.3.0-linux-amd64.tar.gz -o /tmp/roadrunner.tar.gz \
&& tar -xvzf /tmp/roadrunner.tar.gz -C /tmp \
&& mv /tmp/roadrunner-2024.3.0-linux-amd64/rr /var/www/html/rr \
&& chmod +x /var/www/html/rr \
&& rm -rf /tmp/roadrunner.tar.gz /tmp/roadrunner-2024.3.0-linux-amd64
# Set PHP configurations for memory, execution time, and file upload sizes
RUN echo "memory_limit = 1024M" > /usr/local/etc/php/conf.d/custom-php-memlimit.ini; \
echo "max_execution_time = 500" > /usr/local/etc/php/conf.d/custom-php-time.ini; \
echo "post_max_size = 100M" > /usr/local/etc/php/conf.d/custom-php-size.ini;

90
docker/policy.xml Normal file
View File

@ -0,0 +1,90 @@
<!--
Configure ImageMagick policies.
Domains include system, delegate, coder, filter, path, or resource.
Rights include none, read, write, execute and all. Use | to combine them,
for example: "read | write" to permit read from, or write to, a path.
Use a glob expression as a pattern.
Suppose we do not want users to process MPEG video images:
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
Here we do not want users reading images from HTTP:
<policy domain="coder" rights="none" pattern="HTTP" />
The /repository file system is restricted to read only. We use a glob
expression to match all paths that start with /repository:
<policy domain="path" rights="read" pattern="/repository/*" />
Lets prevent users from executing any image filters:
<policy domain="filter" rights="none" pattern="*" />
Any large image is cached to disk rather than memory:
<policy domain="resource" name="area" value="1GP"/>
Use the default system font unless overwridden by the application:
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
Define arguments for the memory, map, area, width, height and disk resources
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
exceeds policy maximum so memory limit is 1GB).
Rules are processed in order. Here we want to restrict ImageMagick to only
read or write a small subset of proven web-safe image types:
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
-->
<policymap>
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
<policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="map" value="512MiB"/>
<policy domain="resource" name="width" value="16KP"/>
<policy domain="resource" name="height" value="16KP"/>
<!-- <policy domain="resource" name="list-length" value="128"/> -->
<policy domain="resource" name="area" value="128MP"/>
<policy domain="resource" name="disk" value="1GiB"/>
<!-- <policy domain="resource" name="file" value="768"/> -->
<!-- <policy domain="resource" name="thread" value="4"/> -->
<!-- <policy domain="resource" name="throttle" value="0"/> -->
<!-- <policy domain="resource" name="time" value="3600"/> -->
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/>
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- <policy domain="system" name="font" value="/path/to/font.ttf"/> -->
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- not needed due to the need to use explicitly by mvg: -->
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
<!-- use curl -->
<policy domain="delegate" rights="none" pattern="URL" />
<policy domain="delegate" rights="none" pattern="HTTPS" />
<policy domain="delegate" rights="none" pattern="HTTP" />
<!-- in order to avoid to get image with password text -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- disable ghostscript format types -->
<policy domain="coder" rights="none" pattern="PS" />
<policy domain="coder" rights="none" pattern="PS2" />
<policy domain="coder" rights="none" pattern="PS3" />
<policy domain="coder" rights="none" pattern="EPS" />
<policy domain="coder" rights="read|write" pattern="PDF" />
<policy domain="coder" rights="none" pattern="XPS" />
</policymap>

28
docker/python/Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
# Set working directory
WORKDIR /app
# Install system dependencies for WeasyPrint
RUN apt-get update && apt-get install -y \
python3-pip \
libpango-1.0-0 \
libharfbuzz0b \
libpangoft2-1.0-0 \
libharfbuzz-subset0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
RUN pip install --no-cache-dir Flask PyMuPDF weasyprint
# Copy application files
COPY ./src/scripts/pdf_manage.py /app/pdf_manage.py
# Expose the Flask port
EXPOSE 5000
# Run Flask server
CMD ["python", "pdf_manage.py"]

54
docker/roadrunner.yaml Normal file
View File

@ -0,0 +1,54 @@
version: '3'
rpc:
# RPC interface for internal communication
listen: tcp://127.0.0.1:6001
server:
# Command to start Laravel Octane server
command: "php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=9001"
http:
# HTTP server configuration
address: 0.0.0.0:9001
pool:
# Worker pool configuration
num_workers: 8
max_jobs: 1000
allocate_timeout: 60s
destroy_timeout: 60s
static:
# Serve static files
dir: ./public
forbid: [ ".php", ".env", ".htaccess" ]
jobs:
num_pollers: 64
timeout: 60
pipeline_size: 100000
driver: redis
redis:
addr: laravel-redis:6379 # Redis host and port
stream: [ "fetch_tracking", "callback_retry", "default", "storage_management" ]
timeout: 10s # Fetch timeout
prefetch: 10
pool:
num_workers: 10
allocate_timeout: 60s
destroy_timeout: 60s
logs:
# Logging configuration
level: debug
mode: development
metrics:
# Metrics for monitoring
address: 0.0.0.0:2112
health:
# Health check endpoint
address: 0.0.0.0:2113

View File

@ -0,0 +1,15 @@
[supervisord]
nodaemon=true
[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/artisan horizon
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/horizon.log
stopwaitsecs=3600

16
docker/xdebug.ini Normal file
View File

@ -0,0 +1,16 @@
zend_extension=xdebug.so
xdebug.remote_enable=1
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.idekey=shipping
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.remote_autostart=1
;xdebug.remote_connect_back=1
xdebug.remote_handler = dbgp
xdebug.remote_mode = req
xdebug.log = /var/www/html/xdebug.log
xdebug.log_level = 10

63
src/.env Normal file
View File

@ -0,0 +1,63 @@
APP_NAME=vat_shipping
VITE_APP_NAME="${APP_NAME}"
APP_URL=https://shipping.dalkove-ovladace.cz
ASSET_URL=https://shipping.dalkove-ovladace.cz
DB_CONNECTION=mariadb
DB_QUEUE_CONNECTION=mariadb
DB_HOST=afrodite.my-devbox.cloud
DB_PORT=3307
DB_DATABASE=shipping
DB_DATABASE_PPS=pps
DB_USERNAME=shipping
DB_PASSWORD=onOa8C263ajmWdRltmLD
APP_KEY=base64:ZbNIu92nsvQmghttxsgjENh6Aqk4xR+o6LU7Wt9mpy8=
QUEUE_CONNECTION=redis
WWWUSER=1000
SAIL_XDEBUG_MODE=debug
LOCATIONIQ_API_KEY=pk.adc1013eb5dbc390a94878110d8b658d
OPENAI_API_KEY=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
SENTRY_LARAVEL_DSN=https://4bf0c732f5474b4518bbae14bd396bbd@o4508397333577728.ingest.de.sentry.io/4508397336789072
SENTRY_TRACES_SAMPLE_RATE=1.0
REDIS_CLIENT=phpredis
CACHE_STORE=redis
REDIS_HOST=laravel-redis
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_CACHE_CONNECTION=cache
REDIS_CACHE_DB=1
OCTANE_SERVER=roadrunner
OCTANE_HTTPS=true
REVERB_APP_ID=246524
REVERB_APP_KEY=w1fhion06ydoahnt5nol
REVERB_APP_SECRET=eamu1je7hrq2gwgocqwc
REVERB_HOST="${APP_URL}"
REVERB_PORT=8080
REVERB_SCHEME=https
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
BROADCAST_CONNECTION=pusher
PUSHER_APP_ID=1914025
PUSHER_APP_KEY=c13c74b64f48f3cfad10
PUSHER_APP_SECRET=dc41870ec7bd07bdea88
PUSHER_APP_CLUSTER=eu
PUSHER_SCHEME=https
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

8
src/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/vendor/
node_modules/
public/build
.idea
/scripts/shipping_env
rr
.rr.yaml

43
src/README.md Normal file
View File

@ -0,0 +1,43 @@
composer require laravel/breeze --dev
php artisan breeze:install
npm install -D tailwindcss postcss autoprefixer flowbite flowbite-react
php artisan db:seed --class=CarrierUserCredentialSeeder
npm i @heroicons/react
npm i @material-tailwind/react
npm install date-fns
composer require spatie/laravel-permission
npm i --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/free-regular-svg-icons
npm install --save @fortawesome/react-fontawesome
sudo apt-get install imagemagick
sudo apt-get install php8.3-soap
composer dump-autoload
npm i material-react-table @mui/material @mui/x-date-pickers @mui/icons-material @emotion/react @emotion/styled use-react-countries
npm install dayjs
CONVERT COMMAND REQUIREMENT
installed imagemagick
modify policy
/etc/ImageMagick-6/policy.xml
<policy domain="coder" rights="read|write" pattern="PDF" />
INSIDE REDIS CONTAINER
chown -R redis:redis /data

View File

@ -0,0 +1,32 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class LabelsProcessed implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $userId;
public $result;
public function __construct($userId, $result)
{
$this->userId = $userId;
$this->result = $result;
}
public function broadcastOn()
{
return new Channel('user.' . $this->userId);
}
public function broadcastAs()
{
return 'label.processed';
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Exceptions;
use Exception;
class ExternalApiException extends Exception
{
protected $message;
protected $code;
public function __construct($message = "External API Error", $reference=null, $code = 500)
{
parent::__construct($message, $code);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Exceptions;
use Exception;
class LabelPrintException extends Exception
{
protected $message;
protected $code;
public function __construct($message = "Label Print API Error", $reference=null, $code = 500)
{
parent::__construct($message, $code);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Exceptions;
use Exception;
class ShipmentException extends Exception
{
protected $message;
protected $code;
public function __construct($message = "Shipment Error", $reference=null, $code = 400)
{
parent::__construct($message, $code);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Factories;
use App\Services\AllegroService;
use App\Services\CPostService;
use App\Services\DHLService;
use App\Services\EurohermesService;
use App\Services\GLSService;
use App\Services\InpostService;
use App\Services\KolosService;
use App\Services\PacketaService;
use App\Services\PPLService;
use App\Services\QDLService;
use App\Services\SpringService;
use App\Services\UPSService;
use GuzzleHttp\Client;
class CarrierServiceFactory
{
public static function create($carrier)
{
$client = new Client();
switch ($carrier->carrier_shortname) {
case 'ups':
return new UPSService($client);
case 'dhl':
return new DHLService($client);
case "gls":
return new GLSService($client);
case "packeta":
return new PacketaService($client);
case "post":
case "balikovna":
return new CPostService($client);
case "eurohermes":
return new EurohermesService($client);
case "inpost":
return new InpostService($client);
case "qdl":
return new QDLService($client);
case "ppl":
return new PPLService($client);
case "kolos":
return new KolosService($client);
case "spring":
return new SpringService($client);
case "allegro":
return new AllegroService($client);
}
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\DB;
function isDuplicate($pID)
{
$duplicate_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 1)
->where('m.id', $pID)
->exists();
return $duplicate_check;
}
function isKaravan($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 62)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isProgrammable($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 58)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isEmptyGeneral($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 3)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isGate($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 20)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isGateProgramming($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->join('vat_warehouse.old_mapping2property as m2p', 'm.id', '=', 'm2p.old_mapping_id')
->where('m2p.physical_item_property_id', 32)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isRolo($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->where('m.physical_item_type_id', 25)
->where('m.id', $pID)
->exists();
return $query_check;
}
function isProductRepair($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->where('m.physical_item_type_id', 48)
->where('m.id', $pID)
->exists();
return $query_check;
}
function getProductID($mID)
{
$externalID = DB::table('vat_warehouse.external_id as ei')
->where('ei.mapping_id', $mID)
->where('ei.eshop_id', 2)
->value('external_id');
return $externalID ?? null;
}
function isGeneralBrand($pID)
{
$query_check = DB::table('vat_warehouse.mapping as m')
->whereIn('m.physical_item_type_id', [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 24, 25, 64, 65, 66, 67, 69, 72, 76, 77, 78, 79])
->where('m.id', $pID)
->exists();
return $query_check;
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Helpers;
function splitStreetAddress($address)
{
$complexPattern = '/^(?:(\d+)\s+(.*?)|(.*?)(\d+(?:[\/-]\d+)?\w*))$/';
if (preg_match($complexPattern, $address, $matches)) {
if (!empty($matches[1])) {
// Number is at the beginning
return ['street' => trim($matches[2]), 'number' => trim($matches[1])];
} else {
// Number is at the end
return ['street' => trim($matches[3]), 'number' => trim($matches[4])];
}
} else {
// Try with the simpler pattern
$simplePattern = '/^(.*?)(\d+.*)$/';
if (preg_match($simplePattern, $address, $matches)) {
return ['street' => trim($matches[1]), 'number' => trim($matches[2])];
}
}
return false;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class ActivityLogController extends Controller
{
public function index()
{
return Inertia::render('Admin/ActivityLog');
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class AdminController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
public function logout(Request $request): RedirectResponse
{
Auth::guard("web")->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
$user = Auth::user();
$redirectRoute = match ($user->roles->pluck('name')->first()) {
'admin' => 'expedice',
'expedice' => 'expedice',
'support' => 'expedice',
'customer' => 'dashboard',
default => 'dashboard'
};
return redirect()->intended(route($redirectRoute, absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
return response()->json(['message' => 'User registered successfully'], 201);
}
public function generateToken(Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string'
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json(['access_token' => $token, 'token_type' => 'Bearer']);
}
public function addRole(Request $request)
{
$request->validate([
'email' => 'required|string|email'
]);
$user = User::where('email', $request->email)->first();
try {
$user->assignRole($request->get('role'));
return response()->json(true);
}
catch (\Exception $e) {
return response()->json(false);
}
}
public function generateTokenInternal(Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
'permissions' => 'array'
]);
$user = User::where('id', $request->user()->id)->first();
$token = $user->createToken('internal_token')->plainTextToken;
return response()->json(['access_token' => $token, 'token_type' => 'Bearer']);
}
public function revokeToken(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out successfully']);
}
}

View File

@ -0,0 +1,227 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Models\CarrierMaster;
use App\Models\CarrierUserCredential;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthorisatonController extends Controller
{
/**
* @throws LabelPrintException
* @throws ExternalApiException
*/
public function authorizeUPS(Request $request)
{
if ($request->has('code')) {
$auth_code = $request->query('code');
$this->fetch_auth_token_ups($auth_code);
}
return response()->json(['message' => 'Shipments are being processed.'], 202);
}
private function fetch_auth_token_ups($auth_code) {
$client = new Client();
// if(config('app.debug')) {
$client_id = "I8yNvkcaMZnBBezmIUDjYtazUnmWco5J1SGOHYrwpSWE2ZvS";
$client_secret = "hjZgJ29U7kJsvsXAOCfzpWXBuipZUWI6GXtRAYjIUO8xkPE9YFQGTeqspoFD7tMC";
$redirect_uri = "https://shipping.dalkove-ovladace.cz/api/authorizeUPS";
$sandbox = false;
// }
// else {
// $this->client_id = "QpRcn5vWFOre7mm62mkvf2uluOazmURvElQ5WpMiafNC7tZq";
// $this->client_secret = "733ttSYyCJwl2WEH5Q5dVizRlmGkS4WCnC4UIEnYA0MIPzJG27DETSdLHodE2tii";
// $this->apiURL = "https://onlinetools.ups.com/api/";
// $this->redirect_uri = urlencode("https://remote-control-world.eu/admin2020/ups_api_auth.php");
// }
try {
$response = $client->post('https://onlinetools.ups.com/security/v1/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'code' => $auth_code,
'redirect_uri' => $redirect_uri,
],
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($client_id . ":" . $client_secret),
],
]);
$data = json_decode($response->getBody(), true);
if (isset($data['response']['errors'])) {
echo "fetch_auth_token Error #:";
var_dump($data['response']['errors']);
return false;
} else {
$bearer_token = $data['access_token'];
$refresh_token = $data['refresh_token'];
// Update or create the bearer token
CarrierUserCredential::updateOrCreate(
[
'user_id' => 1,
'carrier_master_id' => CarrierMaster::where('shortname', 'ups')->first()->id,
'type' => 'access_token',
'name' => 'UPS access token',
"sandbox" => $sandbox
],
[
'value' => $bearer_token,
]
);
// Update or create the refresh token
CarrierUserCredential::updateOrCreate(
[
'user_id' => 1,
'carrier_master_id' => CarrierMaster::where('shortname', 'ups')->first()->id,
'type' => 'refresh_token',
'name' => 'UPS refresh token',
"sandbox" => $sandbox
],
[
'value' => $refresh_token,
]
);
echo "success</br></br>";
echo json_encode($data, JSON_PRETTY_PRINT);
return true;
}
} catch (\Exception $e) {
echo "fetch_auth_token Error #:" . $e->getMessage();
return false;
}
}
public function authorizeAllegro(Request $request)
{
// https://allegro.pl/auth/oauth/authorize?response_type=code&client_id=3e2920423d1b483ab2183f5b6ae5e189&redirect_uri=http://localhost:8000/api/authorizeAllegro
if ($request->has('code')) {
$code = $request->query('code');
$this->fetch_auth_token_allegro($code);
}
return response()->json(['message' => 'Shipments are being processed.'], 202);
}
private function fetch_auth_token_allegro($code) {
$sandbox = false;
$token_object = $this->get_access_token_allegro($code);
$access_token = $this->extend_token_allegro($token_object['refresh_token']);
CarrierUserCredential::updateOrCreate(
[
'user_id' => 1,
'carrier_master_id' => CarrierMaster::where('shortname', 'allegro')->first()->id,
'type' => 'access_token',
'name' => 'Allegro access token',
"sandbox" => $sandbox
],
[
'value' => $access_token['access_token'],
]
);
// Update or create the refresh token
CarrierUserCredential::updateOrCreate(
[
'user_id' => 1,
'carrier_master_id' => CarrierMaster::where('shortname', 'allegro')->first()->id,
'type' => 'refresh_token',
'name' => 'Allegro refresh token',
"sandbox" => $sandbox
],
[
'value' => $access_token['refresh_token'],
]
);
}
private function get_access_token_allegro($code)
{
$authUrl = "https://allegro.pl/auth/oauth/token";
$redirect_uri = "http://localhost:8000/api/authorizeAllegro";
$api_credentials = CarrierUserCredential::getCredentialsByCarrier("allegro");
$client = new Client();
try {
$response = $client->post($authUrl, [
'query' => [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri
],
'headers' => [
'Authorization' => 'Basic ' . base64_encode($api_credentials->get('client_id')->value . ":" . $api_credentials->get('client_secret')->value),
'Accept' => 'application/vnd.allegro.public.v1+json'
]
]);
$tokenObject = json_decode($response->getBody(), true);
return $tokenObject;
} catch (RequestException $e) {
exit("Access token failure GET TOKEN: " . $e->getMessage());
}
}
private function extend_token_allegro($refresh_token)
{
$authUrl = "https://allegro.pl/auth/oauth/token";
$redirect_uri = "http://localhost:8000/api/authorizeAllegro";
$api_credentials = CarrierUserCredential::getCredentialsByCarrier("allegro");
$client = new Client();
try {
$response = $client->post($authUrl, [
'query' => [
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token,
'redirect_uri' => $redirect_uri
],
'headers' => [
'Authorization' => 'Basic ' . base64_encode($api_credentials->get('client_id')->value . ":" . $api_credentials->get('client_secret')->value),
'Accept' => 'application/vnd.allegro.public.v1+json'
]
]);
$tokenObject = json_decode($response->getBody(), true);
return $tokenObject;
} catch (RequestException $e) {
exit("Access token failure EXTEND TOKEN: " . $e->getMessage());
}
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers;
use App\Models\Carrier;
use App\Models\CarrierBasePricing;
use App\Models\CarrierBasePricing2Weight;
use App\Models\CarrierMaster;
use App\Models\ShipmentRequest;
use App\Models\ShipmentStatus;
use App\Models\ShipmentStatusHistory;
use App\Models\User;
use App\Models\UserCarrierPricing;
use App\Models\UserCarrierPricing2Weight;
use App\Models\UserDetails;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class BarcodeScanController extends Controller
{
public function handleScan(Request $request)
{
$scan_action = $request->get('action');
$scanned_data = $request->get('scanned_data');
if (isset($scanned_data['action_overwrite'])) {
$scan_action = $scanned_data['action_overwrite'];
}
if (is_null($scan_action) || is_null($scanned_data)) {
return response()->json([
'result' => false,
'message' => "Invalid request: Input data is empty."
]);
}
try {
if($scan_action === "carrier_daily_parcel_pickup")
{
$carrierMaster = CarrierMaster::find($scanned_data['carrier_master_id']);
$carrierMaster->last_pickup = now();
$carrierMaster->save();
return response()->json([
'result' => true,
'message' => "Carrier: " . $carrierMaster->display_name . " --- DAILY PARCEL PICKUP LOGGED"
]);
}
$shipmentRequest = ShipmentRequest::with(['items', 'currentShipmentStatus'])->findOrFail($scanned_data['shipment_request_id']);
if($shipmentRequest->currentShipmentStatus === ShipmentStatus::STATUS_CANCELED) {
Log::channel('expedice')->info('Scanner - order canceled, alert given! ', [
'shipment_request_id' => $shipmentRequest->id
]);
return response()->json([
'result' => false,
'message' => "STORNO - OBJEDNÁVKU NEEXPEDOVAT, ZRUSIT OBALKU, ZLIKVIDOVAT ŠTÍTEK!!!",
'alert' => true
]);
}
switch ($scan_action) {
case "parcel_packing":
{
$packedStatus = $shipmentRequest->shipmentStatuses()
->where('shipment_status_id', ShipmentStatus::STATUS_PACKED)
->first(['created_at']);
if ($packedStatus) {
Log::channel('expedice')->info('Scanner - DUPLICATE SCANNER ENTRY, alert given! ', [
'shipment_request_id' => $shipmentRequest->id
]);
return response()->json([
'result' => false,
'message' => "ALREADY SCANNED at " . Carbon::parse($packedStatus->created_at)->format('Y-m-d H:i:s') . ", DUPLICATE ENTRY !!!",
'alert' => true,
]);
}
ShipmentStatusHistory::create(
[
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_PACKED,
]
);
$shipmentRequest->createStorageRemovalEntries();
return response()->json([
'result' => true,
'message' => "Shipment request ID: " . $shipmentRequest->id . " --- STATUS PACKING"
]);
}
case "remote_printing":
{
ShipmentStatusHistory::create(
[
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_PRINTING,
]
);
return response()->json([
'result' => true,
'message' => "Shipment request ID: " . $shipmentRequest->id . " --- STATUS PRINTING"
]);
}
case "remote_printing_remains":
{
ShipmentStatusHistory::create(
[
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_PRINTING_UNDONE,
]
);
return response()->json([
'result' => true,
'message' => "Shipment request ID: " . $shipmentRequest->id . " --- STATUS AWAITING PRINTING, UNDONE"
]);
}
case "expedice_delay":
{
ShipmentStatusHistory::create(
[
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_EXPEDICE_DELAY,
]
);
return response()->json([
'result' => true,
'message' => "Shipment request ID: " . $shipmentRequest->id . " --- STATUS EXPEDICE DELAY"
]);
}
case "u_mateje":
{
ShipmentStatusHistory::create(
[
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_EXPEDICE_SPECIAL_HANDLING,
]
);
return response()->json([
'result' => true,
'message' => "Shipment request ID: " . $shipmentRequest->id . " --- STATUS EXPEDICE U MATEJE"
]);
}
}
} catch (\Exception $e) {
return response()->json([
'result' => false,
'message' => $e->getMessage()
]);
}
}
}

View File

@ -0,0 +1,235 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Models\Carrier;
use App\Models\CarrierExtraFeeTypes;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentProcessedBaseFee;
use App\Models\ShipmentProcessedExtraFee;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentRequestInvoice;
use App\Models\ShipmentRequestItem;
use App\Rules\Base64Pdf;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use App\Models\Shipment;
use App\Factories\CarrierServiceFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;
class BatchController extends Controller
{
public function batchList(Request $request)
{
$user = $request->user();
// Get pagination and sorting parameters
$pageIndex = $request->get('pageIndex', 0);
$pageSize = $request->get('pageSize', 25);
$sorting = $request->get('sorting', [["id" => "id", "desc" => "true"]]);
$globalFilter = $request->get('globalFilter', '');
$columnFilters = $request->get('columnFilters', []);
// Initialize the query with relationships
$query = ShipmentRequestBatch::with([
'items.shipmentRequest.errors',
'user',
'carrierMaster',
'latestGeneratedFile'
]);
// Apply filtering based on the user's role
if ($user->hasRole('customer')) {
// Filter by the user's own batches
$query->where('user_id', $user->id);
}
// Apply global filter
if (!empty($globalFilter)) {
$query->where(function ($query) use ($globalFilter) {
if (isset($globalFilter['id'])) {
$query->where('id', 'like', "%{$globalFilter['id']}%");
}
if (isset($globalFilter['user'])) {
$query->orWhereHas('user', function ($q) use ($globalFilter) {
$q->where('name', 'like', "%{$globalFilter['user']}%");
});
}
if (isset($globalFilter['carrier'])) {
$query->orWhereHas('carrierMaster', function ($q) use ($globalFilter) {
$q->where('display_name', 'like', "%{$globalFilter['carrier']}%");
});
}
if (isset($globalFilter['created_at'])) {
$query->where('created_at', 'like', "%{$globalFilter['created_at']}%");
}
});
}
// Apply column filters
if (!empty($columnFilters)) {
foreach ($columnFilters as $key => $col_data) {
$column = $col_data['id'];
$value = $col_data['value'];
if (!empty($value)) {
switch ($column) {
case 'id':
$query->where('id', 'like', "%{$value}%");
break;
case 'user.name':
$query->whereHas('user', function ($q) use ($value) {
$q->whereIn('name', $value);
});
break;
case 'carrier_master.display_name':
$query->whereHas('carrierMaster', function ($q) use ($value) {
foreach($value as $carrier_name) {
$q->orWhere('display_name', 'like', "%{$carrier_name}%");
}
});
break;
case 'processed':
$processedValue = ($value === 'true') ? 1 : 0;
$query->where('processed', $processedValue);
break;
case 'created_at':
$query->where('created_at', 'like', "%{$value}%");
break;
default:
// Ignore unknown column filters
break;
}
}
}
}
// Apply sorting
if (!empty($sorting) && is_array($sorting)) {
foreach ($sorting as $sort) {
if (isset($sort['id']) && isset($sort['desc'])) {
$direction = $sort['desc'] === 'true' ? 'desc' : 'asc';
$query->orderBy($sort['id'], $direction);
}
}
}
// Apply pagination
$totalCount = $query->count();
$batches = $query->skip($pageIndex * $pageSize)->take($pageSize)->get();
// Check if the request is an Inertia request
if ($request->header('X-Inertia')) {
return Inertia::render('Batches', [
'batches' => $batches,
'totalCount' => $totalCount,
]);
}
// For API requests, return a JSON response
return response()->json([
'batches' => $batches,
'totalCount' => $totalCount,
]);
}
public function batchProcessed(Request $request)
{
Log::channel('expedice')->info("Batch marked as processed by - " . $request->user()->id, [
'shipment_request_ids' => $request->get('batchIds')
]);
try {
$batchIds = $request->get('batchIds');
$batchIds = ShipmentRequestBatch::whereIn('id', $batchIds)
->where('processed', 0)
->pluck('id')
->toArray();
// Update only the records that were previously not processed
if (!empty($batchIds)) {
ShipmentRequestBatch::whereIn('id', $batchIds)
->update(['processed' => 1]);
// Fetch the relevant ShipmentRequests and their associated Shipments
$shipmentRequests = ShipmentRequest::whereHas('batchItem', function ($query) use ($batchIds) {
$query->whereIn('shipment_request_batch_id', $batchIds);
})->with('shipment')->get();
// Iterate through each ShipmentRequest
foreach ($shipmentRequests as $shipmentRequest) {
$shipment = $shipmentRequest->shipment;
if (!$shipment) {
continue;
}
$shipment_price_object = $shipmentRequest->carrier->userCarrierPricings()->whereHas('user', function ($query) use ($shipmentRequest) {
$query->where('id', $shipmentRequest->user->id);
})->first();
if (!$shipment_price_object) {
$shipment_price_object = $shipmentRequest->carrier->basePricing()->first();
$user_carrier_grp_uplift = $shipmentRequest->user->userCarrierPricingGroup->carrierPricingGroup->base_ratio;
$shipment_price = round($shipment_price_object->getPriceForWeight((float)$shipmentRequest->weight) + ($shipment_price_object->getPriceForWeight((float)$shipmentRequest->weight) * ($user_carrier_grp_uplift / 100)), 0);
} else {
$shipment_price = $shipment_price_object->getPriceForWeight((float)$shipmentRequest->weight);
}
// Prepare the base fees and extra fees
$baseFeesData = [
'shipment_id' => $shipment->id,
'base_fee_type' => 'shipping',
'base_fee_value' => $shipment_price,
'base_fee_desc' => 'Shipping price',
];
ShipmentProcessedBaseFee::create($baseFeesData);
if (isset($shipmentRequest->cod_value) && is_numeric($shipmentRequest->cod_value) && $shipmentRequest->cod_value >= 0) {
$cod_extra_fee_type = CarrierExtraFeeTypes::where('name', 'COD')->first();
$extraFeesData = [
'shipment_id' => $shipment->id,
'extra_fee_type_id' => $cod_extra_fee_type->id,
'extra_fee_type_value' => $shipment_price_object->extraFees()->where('carrier_extra_fee_type_id', $cod_extra_fee_type->id)->value('carrier_extra_fee_value'),
'extra_fee_desc' => "COD fee",
];
ShipmentProcessedExtraFee::create($extraFeesData);
}
}
return response()->json(['status' => 'success', 'message' => 'Records updated successfully']);
}
else {
return response()->json(['status' => 'success', 'message' => 'Batches already processed in past...']);
}
} catch (\Exception $e) {
return response()->json(['status' => 'error', 'message' => 'Failed to update records', 'error' => $e->getMessage()]);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\ResendShipmentCallbacks;
use Illuminate\Http\JsonResponse;
class CallbackController extends Controller
{
/**
* Trigger the resending of shipment callbacks.
*
* @return JsonResponse
*/
public function resendCallbacks(): JsonResponse
{
ResendShipmentCallbacks::dispatch();
return response()->json(['message' => 'Callback resending job dispatched successfully.']);
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Models\Carrier;
use App\Models\CarrierMaster;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentRequestInvoice;
use App\Models\ShipmentRequestItem;
use App\Rules\Base64Pdf;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use App\Models\Shipment;
use App\Factories\CarrierServiceFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class CarrierController extends Controller {
public function update(Request $request)
{
// Validate the request data
$validatedData = $request->validate([
'id' => 'required|integer',
'internal' => 'required|string|max:255',
'ext_id' => 'nullable|string|max:255',
'carrier_name' => 'required|string|max:255',
'carrier_shortname' => 'nullable|string|max:255',
'pickup_points' => 'nullable|string|max:1000',
'carrier_contract' => 'nullable|string|max:255',
'customs_declarations' => 'nullable|string|max:255',
'cod_allowed' => 'required|boolean',
'cod_enabled' => 'required|boolean',
'cod_price' => 'nullable|numeric|between:0,999999.99',
'country' => 'nullable|string|max:255',
'zone_id' => 'nullable|string|max:255',
'currency' => 'nullable|string|max:3',
'img' => 'nullable|string|max:255',
'enabled_store' => 'required|boolean',
'shipping_price' => 'nullable|numeric|between:0,999999.99',
'free_shipping_enabled' => 'required|boolean',
'delivery_time' => 'nullable|string|max:255',
'display_order' => 'required|integer',
'api_allowed' => 'required|boolean',
]);
// Find the carrier by its ID
$carrier = Carrier::findOrFail($validatedData['id']);
// Update the carrier with the validated data
$carrier->update($validatedData);
// Return a success response
return response()->json(['message' => 'Carrier updated successfully', 'carrier' => $carrier]);
}
public function carrierOptions(Request $request) {
if(sizeof($request->get('shipmentIds')) > 1) {
return response()->json([
'error' => "More than 1 shipment selected!"
]);
}
$shipment = ShipmentRequest::where('id', $request->get('shipmentIds')[0])->first();
$carriers = CarrierMaster::with(['carriers' => function ($query) use ($shipment) {
$query->where(function ($q) use ($shipment) {
$q->where('country', $shipment->delivery_address_country_iso)
->where('enabled_store', true)
->where('pickup_points', 0)
->orWhereIn('carrier_shortname', ['ups', 'post', 'packeta']);
});
}])
->where(function ($query) use ($shipment) {
$query->where('carrier_enabled', true)
->orWhereIn('shortname', ['ups', 'post', 'packeta']);
})
->get();
$carriers = $carriers->filter(function ($carrierMaster) {
return $carrierMaster->carriers->isNotEmpty(); // Only include CarrierMasters with at least 1 carrier
});
return response()->json($carriers);
}
public function carrierFullList(Request $request) {
$carriers = Carrier::with(['carrierMaster'])
->get();
// Check if the request is an Inertia request
if ($request->header('X-Inertia')) {
return Inertia::render('Admin/Carriers', [
'carriers' => $carriers
]);
}
// For API requests, return a JSON response
return response()->json($carriers);
}
public function carrierFilterList(Request $request) {
$carriers = Carrier::all()->pluck("carrier_name");
// For API requests, return a JSON response
return response()->json($carriers);
}
public function carrierDetail($id) {
$carrier = Carrier::with(
[
'carrierMaster',
'basePricing.pricingWeights',
'basePricing.extraFees.extraFeeType'
]
)->find($id);
return Inertia::render('Admin/CarrierDetails', [
'carrier' => $carrier,
]);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers;
use App\Models\Carrier;
use App\Models\CarrierBasePricing;
use App\Models\CarrierBasePricing2Weight;
use App\Models\User;
use App\Models\UserCarrierPricing;
use App\Models\UserCarrierPricing2Weight;
use App\Models\UserDetails;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CarrierPricingController extends Controller {
public function updatePricing(Request $request, $id)
{
// Validate the request data
$request->validate([
'cod_price' => 'required|numeric|min:0', // example validation rule
]);
// Find the record by ID and update the cod_price
$record = CarrierBasePricing::findOrFail($id);
$record->update([
'cod_price' => $request->get('cod_price'),
]);
// Return a success response
return response()->json([
'success' => true,
]);
}
public function updatePricingWeight(Request $request, $id)
{
$record = CarrierBasePricing2Weight::findOrFail($id);
$record->update([
'weight_max' => $request->get('weight_max'),
'shipping_price' => $request->get('shipping_price'),
]);
// Return a success response
return response()->json([
'success' => true,
]);
}
public function deletePricingWeight($id)
{
// Retrieve the record by its ID
$record = CarrierBasePricing2Weight::findOrFail($id);
// Delete the record from the database
$record->delete();
// Return a success response
return response()->json([
'success' => true,
]);
}
public function addPricingWeight(Request $request) {
CarrierBasePricing2Weight::create([
'user_carrier_pricing_id' => $request->get('user_carrier_pricing_id'),
'weight_max' => $request->get('weight_max'),
'shipping_price' => $request->get('shipping_price'),
]);
return response()->json([
'success' => true,
]);
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
class ChatGPTController
{
protected $client;
protected $apiKey;
protected $apiUrl;
public function __construct()
{
$this->client = new Client();
$this->apiKey = env('OPENAI_API_KEY');
$this->apiUrl = 'https://api.openai.com/v1/chat/completions';
}
public function validateAddress(Request $request)
{
$address = $request->input('address');
$structured = $request->input('structured');
if(isset($structured) && $structured) {
$formattedAddress = "{$address['streetName']} {$address['streetNumber']} [{$address['additional_info']}], {$address['city']}, {$address['zip']}, {$address['stateISO']}, {$address['countryISO']}";
}
else {
$formattedAddress = "{$address['address_query']}";
}
$payload = [
'model' => 'gpt-4o-mini',
'messages' => [
[
'role' => 'system',
'content' => "For given address provided try to normalize address. If the address is not correct, try to correct it, but do not make up the data - keep the input data as is, just correct it, do not change it completely.
Return just the json, nothing else, no other texts or explanations as this response will be used for API call response. This is the format for the address:
'streetName'
'streetNumber',
'city',
'zip',
'stateISO',
'countryISO'
if the data is correct, add a 'address_valid': true to the json. If the address was slightly modified, but it is a valid address, use 'address_valid': true and 'address_modified': true. If the address is wrong or very incomplete, unable to be verified or completed,
use 'address_valid': false.
If there are any notes or comments that you think should be made about the address, modification, correction, etc, you can put your comment or reasoning inside 'validation_comments' into the json response.
Some additional rules:
only include state iso if the state is required generaly in postal and delivery APIs, such as US, UK, ES, CA, MX, otherwise you do not need to include it. State ISO is a subiso for the state or region, eq EN in GB (Englang in Great Britain), it is required there.
if the state code is missing and not mandatory / crucial, do not include any mentions of it into the 'validation_comments'
do not put \n into the response or any newline characters, which destroy the json after I read it from this API call
In case a crucial part of the address is missing, but the rest of the address is valid - this is not a valid address. Valid address is an address where a parcel could be delivered to - which
is not possible without crucial parts such as street name, house number, zip code, etc.
In the case a crucial part of the address is missing, add this json object to response:
missing_parts: {
'streetName': bool
'streetNumber': bool,
'city': bool,
'zip': bool,
'stateISO': bool,
'countryISO: bool'
}
The bool should be a boolean value of the result - if the part is missing or significantly incorrect, the bool will be false, otherwise it will be true
The additional info field (in [] brackets) is optional and can contain information like suburb, block, flat, apt number, etc. Sometimes this is where customers submit their house number by accident
take the value from there if you find it helps to determine the address, otherwise ignore it. If you chose to take the value from the additional_info field, please mention in in the 'validation_comments' field.
In case the address is in a different language using different alphabet, the main output should always be in latin.
If this happens, mention that the address was normalized to latin characters in the 'validation_comments' field.
"
],
[
'role' => 'user',
'content' => "Normalize the following address: {$formattedAddress}"
]
]
];
return $this->callAPI($payload);
}
private function callAPI($payload) {
try {
// Make the request
$response = $this->client->post($this->apiUrl, [
'headers' => [
'Authorization' => "Bearer $this->apiKey",
'Content-Type' => 'application/json',
],
'json' => $payload
]);
// Parse the response
$responseData = json_decode($response->getBody(), true);
// return $responseData['choices'][0]['message']['content'];
return response()->json(["response" => json_decode($responseData['choices'][0]['message']['content'])], 201);
} catch (RequestException $e) {
// Handle exceptions
$error = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : $e->getMessage();
echo "Error: $error";
return response()->json(["error" => $error], 201);
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Models\Carrier;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentRequestInvoice;
use App\Models\ShipmentRequestItem;
use App\Rules\Base64Pdf;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use App\Models\Shipment;
use App\Factories\CarrierServiceFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;
class ErrorLogController extends Controller
{
public function errorList(Request $request)
{
$errors = ShipmentErrorLog::with(['shipmentRequest.batch', 'shipmentRequest.carrier'])
->where('resolved', '=', false)
->get()
->map(function ($error) {
$batchId = optional($error->shipmentRequest->batch)->id;
return [
'error' => $error,
'shipment_request' => $error->shipmentRequest,
'batch_id' => $batchId,
];
});
// Check if the request is an Inertia request
if ($request->header('X-Inertia')) {
return Inertia::render('Errors', [
'errors' => $errors,
'batchIds' => $request->get('batchIds')
]);
}
// For API requests, return a JSON response
return response()->json($errors);
}
public function markResolved(Request $request) {
if(is_array($request->get('id'))) {
foreach($request->get('id') as $errorId) {
$error = ShipmentErrorLog::where('id', $errorId)->firstOrFail();
// Update the shipment with the validated data
$error->update([
'resolved' => true,
]);
// Return a success response
}
return response()->json(['message' => 'Shipment updated successfully', 'error' => $request->get('id')]);
}
else {
$error = ShipmentErrorLog::where('id', $request->get('id'))->firstOrFail();
// Update the shipment with the validated data
$error->update([
'resolved' => true,
]);
// Return a success response
return response()->json(['message' => 'Shipment updated successfully', 'error' => $error->id]);
}
}
}

View File

@ -0,0 +1,690 @@
<?php
namespace App\Http\Controllers;
use App\Models\Carrier;
use App\Models\ShipmentRequest;
use App\Models\ShipmentStatus;
use Barryvdh\DomPDF\Facade\Pdf;
use DateTime;
use DateTimeZone;
use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use IntlDateFormatter;
use Metzli\Encoder\Encoder;
use Metzli\Renderer\PngRenderer;
use function App\Helpers\isDuplicate;
use function App\Helpers\isGate;
use function App\Helpers\isGateProgramming;
use function App\Helpers\isKaravan;
use function App\Helpers\isProgrammable;
use function App\Helpers\isRolo;
class ExpediceStickersController extends Controller
{
public function getStickers(Request $request)
{
if (isset($request->get('carrier')['shortname'])) {
$carrierIds = Carrier::where('carrier_shortname', $request->get('carrier')['shortname'])
->pluck('id')
->toArray();
$shipmentRequests = ShipmentRequest::with('items')
->whereIn('carrier_id', $carrierIds)
->whereDoesntHave('shipment')
->get();
} elseif ($request->get('batchIds') !== null) {
$batchIds = $request->get('batchIds');
$shipmentRequests = ShipmentRequest::with('items')
->whereHas('batch', function ($query) use ($batchIds) {
$query->whereIn('id', $batchIds);
})
->get();
} elseif ($request->get('shipmentIds') !== null) {
$shipmentRequests = ShipmentRequest::whereIn('id', $request->get('shipmentIds'))->get();
}
return response($this->generatePDF($this->generateHTML($shipmentRequests)), 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="expedice_stickers.pdf"');
}
public static function getStickerForLabel(ShipmentRequest $shipmentRequest)
{
return (new ExpediceStickersController)->generatePDF((new ExpediceStickersController)->generateHTML_new($shipmentRequest));
}
public function generatePDF(string $html_string)
{
// Initialize Dompdf with options
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
// Load HTML content
$dompdf->loadHtml($html_string);
// Set paper size and orientation
$dompdf->setPaper(array(0, 0, 100, 595), 'landscape');
// Render the PDF
$dompdf->render();
return $dompdf->output();
}
public function generatePDFForLabel(string $html_string)
{
// Initialize Dompdf with options
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
// Load HTML content
$dompdf->loadHtml($html_string);
// Set paper size and orientation
$dompdf->setPaper(array(0, 0, 100, 595), 'landscape');
// Render the PDF
$dompdf->render();
return $dompdf->output();
}
public function generateHTML(Collection $shipmentRequest): string
{
$html_string = "<style>
@page {margin: 0px; }
body { margin: 0px; }
.table {
width: 100%;
border-collapse: collapse;
}
.table td {
width: 69.5mm;
height: 42.2mm;
border: 1px solid black;
text-align: center;
vertical-align: top;
}
.sticker-content {
margin: 2px;
}
.sticker img {
width: 30px;
height: auto;
filter: grayscale(1);
}
.sticker-box {
position: relative;
}
.sticker div {
display: inline-block;
vertical-align: top;
}
tbody > tr:nth-child(1) > td {
padding-top: 12px;
height: calc(42.2mm - 31px);
}
tbody > tr:nth-last-child(1) > td {
padding-bottom: 12px;
height: calc(42.2mm - 31px);
}
tbody > tr > td:nth-child(3n+1) {
padding-left: 18.43px;
width: calc(70mm - 24.5px);
}
/* Targets 3rd, 6th, 9th, etc. */
tbody > tr > td:nth-child(3n+3) {
padding-right: 18.43px;
width: calc(70mm - 24.5px);
}
</style>";
$flags = [
'is_duplicate' => false,
'is_karavan' => false,
'is_programmable' => false,
'is_rolo' => false,
'is_gate' => false,
'is_gate_programming' => false,
];
$counter = 0;
//$orders_final = array(272721, 272690, 272691);
$html_string .= '<table class="table"><tr>';
foreach ($shipmentRequest as $shipment) {
// if ($counter % 21 == 0) {
// $html_string .= '<div class="page">';
// }
if ($counter % 3 == 0 && $counter != 0) {
$html_string .= '</tr><tr>';
}
foreach ($shipment->items as $item) {
if (!empty($item->item_id_internal_warehouse)) {
Log::info($item->item_id_internal_warehouse);
// Use logical OR to set the flags
$flags['is_duplicate'] |= isDuplicate($item->item_id_internal_warehouse);
$flags['is_karavan'] |= isKaravan($item->item_id_internal_warehouse);
$flags['is_programmable'] |= isProgrammable($item->item_id_internal_warehouse);
$flags['is_rolo'] |= isRolo($item->item_id_internal_warehouse);
$flags['is_gate'] |= isGate($item->item_id_internal_warehouse);
$flags['is_gate_programming'] |= isGateProgramming($item->item_id_internal_warehouse);
}
}
Log::info(var_export($flags, true));
$baterky = 0;
$baterky_total = 0;
if (getenv('IS_RCW') == 'true') {
$defaultBattery = 86967;
} else {
$defaultBattery = 18043;
}
$has_batteries = false;
$is_duplicate = false;
$is_gate = false;
$is_luki = false;
$is_programmable = false;
$is_karavan = false;
$allegro_order = false;
foreach ($shipment->items as $item) {
if (($item->HScode == "85068080") || ($item->HScode == "8548102910")) {
$has_batteries = true;
$baterky += $item->quantity;
$baterky_total += round($item->price);
continue;
}
$is_duplicate = $flags['is_duplicate'];
$is_karavan = $flags['is_karavan'];
$is_programmable = $flags['is_programmable'] && !$flags['is_rolo'];
$is_gate = $flags['is_gate'];
$is_luki = $flags['is_gate_programming'];
if ($is_gate) {
$is_programmable = false;
}
if ($is_luki) {
$is_gate = false;
$is_programmable = false;
}
if ($shipment->carrier->carrier_shortname === "allegro") {
$allegro_order = true;
}
}
$html_string .= '<td class="sticker">
<div class="sticker-box">
<div style="position: absolute;
top: 0px;
right: 2px;">
<img src="/includes/templates/vat_responsive/images/shipping/small_icons/' . $shipment->carrier->img . '"
style="width: 30px; height: auto; filter: grayscale(1);">
</div>
<div style="position: absolute;
top: 0px;
left: 2px;">';
if ($is_programmable) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
N
</div>';
}
if ($is_karavan) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
K
</div>';
}
if ($is_luki) {
$html_string .= '<div style="width: 37px; height: 23px; font-size: 20px; line-height: 22px;">
G-P
</div>';
}
if ($is_gate) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
G
</div>';
}
if ($is_duplicate) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
D
</div>';
}
if ($has_batteries) {
$html_string .= '<div style="width: 50px; height: 23px; font-size: 16px; line-height: 25px; color: red;">
B-' . $baterky . 'x
</div>';
}
if ($allegro_order) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
|A|
</div>';
}
$today = new DateTime('now', new DateTimeZone('Europe/Prague'));
// Create an IntlDateFormatter
$formatter = new IntlDateFormatter(
'cs-CZ',
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
new DateTimeZone('Europe/Prague'),
IntlDateFormatter::GREGORIAN,
'EEE' // Pattern to get the full name of the day
);
// Format the timestamp
$dayOfWeek = $formatter->format($today);
$html_string .= '<div style="width: 20px; height: 20px; font-size: 10px; line-height: 20px;">
' . $dayOfWeek . '
</div>
</div>
<div style="font-size: 14px; font-weight: bold; padding-top: 20px;">
<div>' . $shipment->shipment_reference . '</div>
<div style="margin-left: 1.5em;">' . $shipment->delivery_address['name'] . '</div>
</div>';
$overSize = false;
if ((sizeof($shipment->items) > 2 && !$has_batteries) || sizeof($shipment->items) > 3) {
$overSize = true;
}
if ($overSize) {
?>
<div
style="font-size: 12px; margin-bottom: 0.3em; margin-top: 0.2em; width: 90%; margin-left: auto; margin-right: auto;">
V objednavce je <?= sizeof($shipment->items) ?> produktu, je treba overit v systemu.
</div>
<?php
} else {
foreach ($shipment->items as $item) {
if (($item->HScode == "85068080") || ($item->HScode == "8548102910")) {
$has_batteries = true;
$baterky += $item->quantity;
$baterky_total += round($item->price);
continue;
}
if ($item->quantity > 1) {
$style = "color: #00bf00; font-weight: bold; font-size: 12px;";
} else {
$style = "";
}
$html_string .= '<div
style="font-size: 11px; margin-bottom: 0.3em; margin-top: 0.2em; width: 90%; margin-left: auto; margin-right: auto;">
<div style="' . $style . '">' . $item->quantity . "x - " . $item->name . '</div>
</div>
<div style="font-style: italic;border-bottom: 1px solid grey; padding-bottom: 0.2em;">
<div>
' . $item->model_number . '
</div>
<div style="margin-left: 1.5em; font-size: 14px;">
Price: ' . round($item->price, 0) . " " . $shipment->currency . '
</div>
</div>';
}
}
$html_string .= '</div></td>';
$counter++;
}
while ($counter % 21 != 0) {
if ($counter % 3 == 0) {
$html_string .= "</tr><tr>";
}
$html_string .= '<td class="sticker" style="border: none;"></td>';
$counter++;
}
$html_string .= '</tr></table>';
return $html_string;
}
public function generateHTML_new(ShipmentRequest $shipment): string
{
$html_string = "<style>
@page {margin: 0px; }
body { margin: 0px; }
.sticker-content {
margin: 2px;
}
.sticker img {
width: 30px;
height: auto;
filter: grayscale(1);
}
.sticker-box {
position: relative;
}
.sticker div {
display: inline-block;
vertical-align: top;
}
table.sticker-table {
width: 100%;
border-collapse: collapse;
}
table.sticker-table td {
vertical-align: top;
padding: 2px;
}
</style>";
$flags = [
'is_duplicate' => false,
'is_karavan' => false,
'is_programmable' => false,
'is_rolo' => false,
'is_gate' => false,
'is_gate_programming' => false,
];
foreach ($shipment->items as $item) {
if (!empty($item->item_id_internal_warehouse)) {
Log::info($item->item_id_internal_warehouse);
// Use logical OR to set the flags
$flags['is_duplicate'] |= isDuplicate($item->item_id_internal_warehouse);
$flags['is_karavan'] |= isKaravan($item->item_id_internal_warehouse);
$flags['is_programmable'] |= isProgrammable($item->item_id_internal_warehouse);
$flags['is_rolo'] |= isRolo($item->item_id_internal_warehouse);
$flags['is_gate'] |= isGate($item->item_id_internal_warehouse);
$flags['is_gate_programming'] |= isGateProgramming($item->item_id_internal_warehouse);
}
}
if ($flags['is_duplicate'] && $flags['is_programmable'] && sizeof($shipment->items) > 1) {
$flags['is_duplicate'] = false;
$flags['is_programmable'] = false;
}
Log::info(var_export($flags, true));
$baterky = 0;
$baterky_total = 0;
if (getenv('IS_RCW') == 'true') {
$defaultBattery = 86967;
} else {
$defaultBattery = 18043;
}
$has_batteries = false;
$is_duplicate = false;
$is_gate = false;
$is_luki = false;
$is_programmable = false;
$is_karavan = false;
$allegro_order = false;
foreach ($shipment->items as $item) {
if (($item->HScode == "85068080") || ($item->HScode == "8548102910")) {
$has_batteries = true;
$baterky += $item->quantity;
$baterky_total += round($item->price);
continue;
}
$is_duplicate = $flags['is_duplicate'];
$is_karavan = $flags['is_karavan'];
$is_programmable = $flags['is_programmable'] && !$flags['is_rolo'];
$is_gate = $flags['is_gate'];
$is_luki = $flags['is_gate_programming'];
if ($is_gate) {
$is_programmable = false;
}
if ($is_luki) {
$is_gate = false;
$is_programmable = false;
}
if ($shipment->carrier->carrier_shortname === "allegro") {
$allegro_order = true;
}
}
$code = Encoder::encode(json_encode(
[
"shipment_request_id" => $shipment->id
]
));
$renderer = new PngRenderer();
$aztec_code = $renderer->render($code);
$base64_aztec_img = base64_encode($aztec_code);
$base64_aztec_src = "data:image/png;base64,{$base64_aztec_img}";
$html_string .= '<div class="sticker">
<div class="sticker-box">
<table class="sticker-table">
<tr>
<td>
<img src="' . $base64_aztec_src . '" style="width: 80px; height: 80px; margin-left: 3px;">
</td>
<td style="font-size: 12px; font-weight: bold; padding-top: 19px;">
<table>
<tr><td>' . $shipment->shipment_reference . '</td></tr>
<tr><td>' . $shipment->delivery_address['name'] . '</td></tr>
</table>
</td>';
$overSize = false;
if ((sizeof($shipment->items) > 2 && !$has_batteries) || sizeof($shipment->items) > 3) {
$overSize = true;
}
if ($overSize) {
$html_string .= '<td
style="font-size: 12px; margin-bottom: 0.3em; margin-top: 0.2em; width: 90%; margin-left: auto; margin-right: auto;">
V objednavce je ' . sizeof($shipment->items) . ' produktu, je treba overit v systemu.
</td>';
} else {
$html_string .= "<td><table style='border-collapse: collapse; '>";
foreach ($shipment->items as $item) {
if (($item->HScode == "85068080") || ($item->HScode == "8548102910")) {
$has_batteries = true;
$baterky += $item->quantity;
$baterky_total += round($item->price);
continue;
}
if ($item->quantity > 1) {
$style = "color: #00bf00; font-weight: bold; font-size: 12px;";
} else {
$style = "";
}
$html_string .= '<tr
style="font-size: 10px; margin-bottom: 0.3em; margin-top: 0.2em; width: 90%; margin-left: auto; margin-right: auto;">
<td style="' . $style . '">' . $item->quantity . "x - " . $item->name . '</td>
</tr>
<tr style="font-style: italic;border-bottom: 1px solid grey; padding-bottom: 0.2em; font-size: 12px;">
<td>
' . $item->model_number . '
</td>
<td style="margin-left: 1.5em; font-size: 14px;">
Price: ' . round($item->price, 0) . " " . $shipment->currency . '
</td>
</tr>';
}
$html_string .= "</table></td>";
}
$img_data = Storage::disk('public')->get("images/shipping/{$shipment->carrier->carrierMaster->img}");
$base64_img = base64_encode($img_data);
$base64_src = "data:image/png;base64,{$base64_img}";
$html_string .= '<td><table class="sticker-table">
<tr>
<td>
<img src="' . $base64_src . '" style="width: 25px; height: 25px; filter: grayscale(1);">
</td>
<td>';
if ($is_programmable) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
N
</div>';
}
if ($is_karavan) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
K
</div>';
}
if ($is_luki) {
$html_string .= '<div style="width: 37px; height: 23px; font-size: 20px; line-height: 22px;">
G-P
</div>';
}
if ($is_gate) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
G
</div>';
}
if ($is_duplicate) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
D
</div>';
}
if ($has_batteries) {
$html_string .= '<div style="width: 50px; height: 23px; font-size: 16px; line-height: 25px; color: red;">
B-' . $baterky . 'x
</div>';
}
if ($allegro_order) {
$html_string .= '<div style="width: 23px; height: 23px; font-size: 23px; line-height: 23px;">
|A|
</div>';
}
$today = new DateTime('now', new DateTimeZone('Europe/Prague'));
// Create an IntlDateFormatter
$formatter = new IntlDateFormatter(
'cs-CZ',
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
new DateTimeZone('Europe/Prague'),
IntlDateFormatter::GREGORIAN,
'EEE' // Pattern to get the full name of the day
);
// Format the timestamp
$dayOfWeek = $formatter->format($today);
$html_string .= '<div style="width: 20px; height: 20px; font-size: 10px; line-height: 20px;">
' . $dayOfWeek . '
</div>
</td>
</tr>
</table></td>';
$html_string .= '</tr></table></div></div>';
return $html_string;
}
}

View File

@ -0,0 +1,427 @@
<?php
namespace App\Http\Controllers;
use App\Factories\CarrierServiceFactory;
use App\Models\Carrier;
use App\Models\CarrierMaster;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentStatus;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use function App\Helpers\isGate;
use function App\Helpers\isEmptyGeneral;
use function App\Helpers\isProductRepair;
class ExpediceStocklistController extends Controller
{
private $room_prio = [
"FIK" => 100000,
"PRD" => 200000,
"DLN" => 300000,
"XKB" => 400000,
"KNCL" => 500000,
"KCH" => 600000,
"TSK" => 700000,
"SKL" => 800000
];
private function evalRank($array)
{
$rank = 0;
try {
if (array_key_exists($array[0], $this->room_prio)) {
$rank += $this->room_prio[$array[0]];
$rank += ord(substr($array[1], 0, 1)) * 1000;
$rank += intval(substr($array[1], 1, 1)) * 100;
$rank += intval($array[2]);
} else {
$rank = 1000000;
}
}
catch (\Exception $e) {
$rank = 1000000;
}
return $rank;
}
private function assignRank(&$orders)
{
foreach ($orders as $order) {
$rank = 1000000;
foreach($order->items as $item_shipment_request) {
try {
if($order->user->name === "dalkoveovladace") {
$eshop_id = 1;
}
else {
$eshop_id = 2;
}
$url = "https://sklad.dalkove-ovladace.cz/api/warehouse/".$eshop_id."/";
$json = file_get_contents($url . $item_shipment_request->item_id_external);
$warehouse_obj = json_decode($json);
// $this->batteries_info = unserialize(stripslashes($query_data->fields['batteries_info']));
$warehouseData = [];
foreach ($warehouse_obj as $item) {
//$this->warehouseData[$item->modelNumber] = $item->warehouseItems;
foreach ($item->warehouseItems as $sklad_pozice) {
if (isset($warehouseData[$item->modelNumber])) {
$warehouseData[$item->modelNumber]['location'] .= " " . $sklad_pozice->location;
} else {
$warehouseData[$item->modelNumber] = array(
"location" => $sklad_pozice->location
);
}
$arr = array(
'location' => $sklad_pozice->location,
'count' => $sklad_pozice->count,
'physicalItemName' => $item->physicalItemName);
$stockData[$item->modelNumber][] = $arr;
}
}
$item_shipment_request->setAttribute('stockData', $stockData);
unset($stockData);
$sklad = $warehouseData;
if (isset($order->items) && sizeof($order->items) > 1) {
$rank = 1000000;
} else {
foreach ($sklad as $s) {
$locations = explode(" ", $s['location']);
foreach ($locations as &$location) {
$location = explode('-', $location);
$rank_new = $this->evalRank($location);
if ($rank > $rank_new) {
$rank = $rank_new;
}
}
}
}
$order->rank = $rank;
} catch (\Exception $e) {
$order->rank = $rank;
$item_shipment_request->setAttribute('stockData', []);
unset($stockData);
continue;
}
}
}
}
private function osort(&$array, $properties)
{
if (is_string($properties)) {
$properties = [$properties => SORT_ASC];
}
$sorted = $array->sort(function ($a, $b) use ($properties) {
foreach ($properties as $k => $v) {
if (is_int($k)) {
$k = $v;
$v = SORT_ASC;
}
$collapse = function ($node, $props) {
foreach ((array)$props as $prop) {
$node = $node->$prop ?? null;
}
return $node;
};
$aProp = $collapse($a, $k);
$bProp = $collapse($b, $k);
if ($aProp != $bProp) {
return ($v == SORT_ASC) ? strnatcasecmp($aProp, $bProp) : strnatcasecmp($bProp, $aProp);
}
}
return 0;
});
return true;
}
private function sortByStockPos(&$orders)
{
$this->assignRank($orders);
$this->osort($orders, 'rank');
}
public function sortOrders(Request $request)
{
if (isset($request->get('carrier')['shortname'])){
$carrierIds = Carrier::where('carrier_shortname', $request->get('carrier')['shortname'])
->pluck('id')
->toArray();
$shipmentRequests = ShipmentRequest::with('items')
->whereIn('carrier_id', $carrierIds)
->whereDoesntHave('shipment')
->get();
}
elseif($request->get('batchIds') !== null) {
$batchIds = $request->get('batchIds');
$shipmentRequests = ShipmentRequest::with('items')
->whereHas('batch', function ($query) use ($batchIds) {
$query->whereIn('id', $batchIds);
})
->get();
}
elseif($request->get('shipmentIds') !== null) {
$shipmentRequests = ShipmentRequest::whereIn('id', $request->get('shipmentIds'))->get();
}
Log::channel('expedice')->info("Stocklist printed by - " . $request->user()->id, [
'shipment_request_ids' =>$shipmentRequests->pluck('id')->toArray()
]);
$this->sortByStockPos($shipmentRequests);
return response($this->generatePDF($this->generateHTML($shipmentRequests)), 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="expedice_stocklist.pdf"');
}
public function startExpedition(Request $request)
{
$limit = $request->get('limit', 30);
$userId = $request->get('userId');
$carrier_shortname = $request->get('carrier')['carrier_shortname'];
if (isset($carrier_shortname)) {
$carrierIds = Carrier::where('carrier_shortname', $carrier_shortname)
->pluck('id')
->toArray();
$shipmentRequests = ShipmentRequest::with('items')
->whereIn('carrier_id', $carrierIds)
->when($userId, function ($query, $userId) {
// Only include this condition if $userId is not null
return $query->where('user_id', $userId);
})
->whereDoesntHave('shipment')
->where('send_again_flag', false)
->whereDoesntHave('batch')
->whereHas('currentShipmentStatus', function ($query) {
$query->whereNotIn('shipment_status_id', [
ShipmentStatus::STATUS_ADDRESS_INSUFFICIENT,
ShipmentStatus::STATUS_OUT_OF_STOCK,
ShipmentStatus::STATUS_IS_REPAIR,
ShipmentStatus::STATUS_PROGRAMMING_MODEL_REQUIRED,
ShipmentStatus::STATUS_PROGRAMMING_PROCESS,
ShipmentStatus::STATUS_CANCELED,
]);
})
->limit($limit)
->get();
} elseif ($request->get('batchIds') !== null) {
$batchIds = $request->get('batchIds');
$shipmentRequests = ShipmentRequest::with('items')
->whereHas('batch', function ($query) use ($batchIds) {
$query->whereIn('id', $batchIds);
})
->whereHas('currentShipmentStatus', function ($query) {
$query->whereNotIn('shipment_status_id', [
ShipmentStatus::STATUS_ADDRESS_INSUFFICIENT,
ShipmentStatus::STATUS_OUT_OF_STOCK,
ShipmentStatus::STATUS_IS_REPAIR,
ShipmentStatus::STATUS_PROGRAMMING_MODEL_REQUIRED,
ShipmentStatus::STATUS_PROGRAMMING_PROCESS,
ShipmentStatus::STATUS_CANCELED,
]);
})
->limit($limit)
->get();
}
elseif($request->get('shipmentIds') !== null) {
$shipmentRequests = ShipmentRequest::whereIn('id', $request->get('shipmentIds'))->get();
}
Log::channel('expedice')->info("Expedice started by - " . $request->user()->id, [
'shipment_request_ids' =>$shipmentRequests->pluck('id')->toArray()
]);
$this->sortByStockPos($shipmentRequests);
$batch = ShipmentRequestBatch::create([
'user_id' => $request->user()->id,
'carrier_master_id' => CarrierMaster::where('shortname', $carrier_shortname)->first()->id,
]);
foreach ($shipmentRequests as $shipment_request) {
// Instantiate the correct carrier service dynamically for each shipment
// $carrierService = CarrierServiceFactory::create($shipment_request->carrier);
ShipmentRequestBatchItem::create([
'shipment_request_batch_id' => $batch->id,
'shipment_request_id' => $shipment_request->id,
]);
}
try {
return response($this->generatePDF($this->generateHTML($shipmentRequests)), 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="expedice_stocklist.pdf"');
}
catch(\Exception $e) {
return response()->json(['errors' => [$e->getMessage()]], 400);
}
}
public function generateHTML(Collection $shipmentRequest): string
{
$html_string = "";
$count_o = 0;
foreach ($shipmentRequest as $shipment_request) {
$color = "";
$hasGate = false;
$hasEmptyGeneral = false;
$isRepair = false;
$isWelux = false;
foreach ($shipment_request->items as $item) {
if (!empty($item->item_id_internal_warehouse)) {
if(isGate($item->item_id_internal_warehouse)) {
$hasGate = true;
}
if(isEmptyGeneral($item->item_id_internal_warehouse)) {
$hasEmptyGeneral = true;
}
if(isProductRepair($item->item_id_internal_warehouse)) {
$isRepair = true;
}
if(in_array($item->item_id_internal_warehouse, [16965, 19791])) {
$isWelux = true;
}
}
}
$bg = $hasGate ? "background: #dbdbdb;" : "";
$bg_general = $hasEmptyGeneral ? "background: #ffb2b2;" : "";
$bg_repair = $isRepair ? "background: #9cd5f7;" : "";
$bg_general = $isWelux ? "background: #ff8080;" : $bg_general;
$html_string .= '<div class="order" style="padding: 5px 10px 5px 10px; font-size: 12px; page-break-inside: avoid; ' . $bg_general . $bg_repair . '">';
$html_string .= '<div style="font-size: 25px; font-weight: bold; ' . $color . ' ' . $bg . '">Obj.: ' . $shipment_request->shipment_reference . '</div>';
$count_p = 0;
foreach ($shipment_request->items as $item) {
if ($count_o % 2) {
$color = $count_p % 2 ? "background-color: #878787;" : "";
} else {
$color = $count_p % 2 ? "background-color: #ffaa0e;" : "";
}
$html_string .= '
<div class="product" style="width: 100%; margin-bottom: 10px; page-break-inside: avoid; ' . $color . '">
<div style="width: 100%; page-break-inside: avoid;">
<div style="width: 100%; page-break-inside: avoid;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="border: 1px solid black; padding: 5px 5px 5px 5px; text-align: center;">' . $item->quantity . 'x</td>
<td style="border: 1px solid black; padding: 5px 5px 5px 5px; width: 65%; text-align: center;">' . $item->name . '</td>
<td style="border: 1px solid black; padding: 5px 5px 5px 5px; width: 25%; text-align: center;">' . $item->model_number . '</td>
<td style="border: 1px solid black; padding: 5px 5px 5px 5px; width: 10%; text-align: center;">' . $item->price . '</td>
</tr>
</table>
</div>
<div style="width: 100%; page-break-inside: avoid;">
<div style="border: 1px solid black; padding: 10px 5px 10px 5px; width: 100%; overflow: hidden;">
<table style="width: 100%; border-collapse: collapse;">
<tr>';
foreach ($item->stockData as $item_model => $stock_positions) {
foreach ($stock_positions as $stock_data) {
$html_string .= '
<td style="padding: 5px 5px 5px 5px; width: 50%; vertical-align: top;">
<div style="font-weight: bold;">';
if (strpos($item_model, $stock_data['physicalItemName']) !== false) {
$html_string .= $item_model;
} else {
$html_string .= $stock_data['physicalItemName'] . " (" . $item_model . ")";
}
$html_string .= '</div>
<div>' . $stock_data['location'] . ", " . $stock_data['count'] . " ks" . '</div>
</td>';
}
}
$html_string .= '
</tr>
</table>
</div>
<div style="padding: 5px 5px 5px 5px; border: none; width: 100%; page-break-inside: avoid;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="border: 1px solid black; padding: 3px 5px 3px 5px; text-align: center;">' . $item->item_note . '</td>
<td style="border: 1px solid black; padding: 3px 5px 3px 5px; text-align: center;">' . $item->shipment->note . '</td>
</tr>
</table>
</div>
</div>
</div>
</div>';
$count_p++;
}
$html_string .= '</div>';
$count_o++;
}
return $html_string;
}
public function generatePDF(string $html_string)
{
try {
$client = new Client();
$response = $client->post('http://pdf_manager:5000/html2pdf', [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'html_content' => $html_string,
],
]);
$pdfContent = $response->getBody()->getContents();
return $pdfContent;
} catch (\Exception $e) {
throw $e;
}
}
}

View File

@ -0,0 +1,708 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Jobs\ReprintLabelsJob;
use App\Models\ActionLog;
use App\Models\Carrier;
use App\Models\CarrierExtraFeeTypes;
use App\Models\CarrierMaster;
use App\Models\Label;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentProcessedBaseFee;
use App\Models\ShipmentProcessedExtraFee;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentRequestInvoice;
use App\Models\ShipmentRequestItem;
use App\Models\ShipmentStatus;
use App\Models\ShipmentStatusHistory;
use App\Models\UserDetails;
use App\Rules\Base64Pdf;
use App\Services\CallbackService;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use App\Models\Shipment;
use App\Factories\CarrierServiceFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;
class LabelController extends Controller
{
protected CallbackService $callbackService;
public function __construct(CallbackService $callbackService)
{
$this->callbackService = $callbackService;
}
private $validateArray = [
'shipments' => 'required|array|min:1',
'shipments.*.shipmentReference' => 'required|string|max:255',
'shipments.*.note' => 'nullable|string|max:1000',
'shipments.*.delivery_address' => 'required|array',
'shipments.*.delivery_address.name' => 'required|string|max:255',
'shipments.*.delivery_address.companyName' => 'nullable|string|max:255',
'shipments.*.delivery_address.streetName' => 'required|string|max:255',
'shipments.*.delivery_address.streetNumber' => 'required|string|max:10',
'shipments.*.delivery_address.city' => 'required|string|max:255',
'shipments.*.delivery_address.zip' => 'required|string|max:20',
'shipments.*.delivery_address.stateISO' => 'nullable|string|max:10',
'shipments.*.delivery_address.countryISO' => 'required|string|max:3',
'shipments.*.pickupPointCode' => 'nullable|string|max:255',
'shipments.*.pickupPointName' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.streetName' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.streetNumber' => 'nullable|string|max:10',
'shipments.*.pickupPointAddress.addressLine2' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.city' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.zip' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.stateISO' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.countryISO' => 'nullable|string|max:255',
'shipments.*.postnummer' => 'nullable|string|max:255',
'shipments.*.contactEmail' => 'nullable|email|max:255',
'shipments.*.contactTelephone' => 'nullable|string|max:20',
'shipments.*.codValue' => 'nullable|numeric',
'shipments.*.codVariableSymbol' => 'nullable|string|max:8',
'shipments.*.shipmentValue' => 'required|numeric',
'shipments.*.currency' => 'required|string|max:3',
'shipments.*.weight' => 'required|numeric',
'shipments.*.items' => 'required|array|min:1',
'shipments.*.items.*.name' => 'required|string|max:255',
'shipments.*.items.*.item_note' => 'nullable|string',
'shipments.*.items.*.HSCode' => 'nullable|string|max:255',
'shipments.*.items.*.model_number' => 'nullable|string|max:255',
'shipments.*.items.*.price' => 'required|numeric',
'shipments.*.items.*.quantity' => 'required|integer',
'shipments.*.items.*.originCountry' => 'required|string|max:3',
'shipments.*.items.*.weight' => 'required|numeric',
'shipments.*.items.*.item_id_internal_warehouse' => 'nullable|numeric',
'shipments.*.items.*.item_id_external' => 'nullable|numeric',
'shipments.*.carrier_id' => 'required|string|max:255',
'shipments.*.allegro_carrier_id' => 'nullable|string|max:255',
'shipments.*.packageType' => 'required|in:ENVELOPE,PACKAGE',
'shipments.*.sendAgainFlag' => 'nullable|boolean',
'shipments.*.allegroSmart' => 'nullable|boolean',
'shipments.*.parcelSize' => 'required|array',
'shipments.*.parcelSize.width' => 'required|string|max:255',
'shipments.*.parcelSize.length' => 'required|string|max:255',
'shipments.*.parcelSize.height' => 'required|string|max:255',
'shipments.*.invoice_data' => 'nullable|array',
'shipments.*.invoice_data.invoiceNumber' => 'nullable|string|max:255',
'shipments.*.invoice_data.date' => 'nullable|string',
'shipments.*.invoice_data.currency' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.name' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.companyName' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.companyVatNumber' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.streetName' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.streetNumber' => 'nullable|string|max:10',
'shipments.*.invoice_data.address.city' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.zip' => 'nullable|string|max:20',
'shipments.*.invoice_data.address.stateISO' => 'nullable|string|max:10',
'shipments.*.invoice_data.address.countryISO' => 'nullable|string|max:3'
];
//TODO: custom validation
// 1. UPS pickup data required for APP only users
// 2. UPS invoice pdf required for UPS nonEU
/**
* @throws LabelPrintException
* @throws ExternalApiException
*/
public function createLabels(Request $request)
{
// Validate the request for an array of shipments
$base64PdfRule = new Base64Pdf();
$this->validateArray['shipments.*.invoice_data.invoice_file'] = ['nullable', $base64PdfRule];
$validator = Validator::make($request->all(), $this->validateArray);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
// Proceed with the validated data
$validatedData = $validator->validated();
$labels = [];
foreach ($validatedData['shipments'] as $shipmentData) {
// Create shipment request
$shipment_request = ShipmentRequest::create([
'shipment_reference' => $shipmentData['shipmentReference'],
'user_id' => $request->user()->id,
'note' => $shipmentData['note'] ?? null,
'delivery_address_name' => $shipmentData['delivery_address']['name'],
'delivery_address_company_name' => $shipmentData['delivery_address']['companyName'] ?? null,
'delivery_address_street_name' => $shipmentData['delivery_address']['streetName'],
'delivery_address_street_number' => $shipmentData['delivery_address']['streetNumber'],
'delivery_address_address_line_2' => $shipmentData['delivery_address']['addressLine2'] ?? null,
'delivery_address_city' => $shipmentData['delivery_address']['city'],
'delivery_address_zip' => $shipmentData['delivery_address']['zip'],
'delivery_address_state_iso' => $shipmentData['delivery_address']['stateISO'] ?? null,
'delivery_address_country_iso' => $shipmentData['delivery_address']['countryISO'],
'pickup_point_code' => $shipmentData['pickupPointCode'] ?? null,
'pickup_point_address_street_name' => $shipmentData['pickupPointAddress']['streetName'] ?? null,
'pickup_point_address_street_number' => $shipmentData['pickupPointAddress']['streetNumber'] ?? null,
'pickup_point_address_city' => $shipmentData['pickupPointAddress']['city'] ?? null,
'pickup_point_address_zip' => $shipmentData['pickupPointAddress']['zip'] ?? null,
'pickup_point_address_state_iso' => $shipmentData['pickupPointAddress']['stateISO'] ?? null,
'pickup_point_address_country_iso' => $shipmentData['pickupPointAddress']['countryISO'] ?? null,
'contact_email' => $shipmentData['contactEmail'] ?? null,
'contact_telephone' => $shipmentData['contactTelephone'] ?? null,
'cod_value' => $shipmentData['codValue'] ?? null,
'cod_variable_symbol' => $shipmentData['codVariableSymbol'] ?? null,
'shipment_value' => $shipmentData['shipmentValue'],
'currency' => $shipmentData['currency'],
'weight' => $shipmentData['weight'],
'width' => $shipmentData['parcelSize']['width'],
'length' => $shipmentData['parcelSize']['length'],
'height' => $shipmentData['parcelSize']['height'],
'carrier_id' => $shipmentData['carrier_id'],
'allegro_carrier_id' => $shipmentData['allegro_carrier_id'] ?? null,
'package_type' => $shipmentData['packageType'],
'send_again_flag' => $shipmentData['sendAgainFlag'] ?? false,
'allegro_smart' => $shipmentData['allegroSmart'] ?? false,
'postnummer' => $shipmentData['postnummer'] ?? null,
]);
// Create shipment items
foreach ($shipmentData['items'] as $itemData) {
ShipmentRequestItem::create([
'shipment_request_id' => $shipment_request->id,
'name' => $itemData['name'],
'item_note' => $itemData['item_note'] ?? null,
'model_number' => $itemData['model_number'] ?? null,
'HSCode' => $itemData['HSCode'] ?? null,
'price' => $itemData['price'],
'quantity' => $itemData['quantity'],
'originCountry' => $itemData['originCountry'],
'weight' => $itemData['weight'],
'item_id_internal_warehouse' => $itemData['item_id_internal_warehouse'] ?? null,
'item_id_external' => $itemData['item_id_external'] ?? null,
]);
}
if (!empty($shipmentData['invoice_data'])) {
ShipmentRequestInvoice::create([
'shipment_request_id' => $shipment_request->id,
'invoice_number' => $shipmentData['invoice_data']['invoiceNumber'],
'invoice_date' => $shipmentData['invoice_data']['date'],
'invoice_currency' => $shipmentData['invoice_data']['currency'],
'invoice_address_name' => $shipmentData['invoice_data']['address']['name'],
'invoice_address_company_name' => $shipmentData['invoice_data']['address']['companyName'] ?? null,
'invoice_address_company_vat_number' => $shipmentData['invoice_data']['address']['companyVatNumber'] ?? null,
'invoice_address_street_name' => $shipmentData['invoice_data']['address']['streetName'],
'invoice_address_street_number' => $shipmentData['invoice_data']['address']['streetNumber'],
'invoice_address_city' => $shipmentData['invoice_data']['address']['city'],
'invoice_address_zip' => $shipmentData['invoice_data']['address']['zip'],
'invoice_address_state_iso' => $shipmentData['invoice_data']['address']['stateISO'] ?? null,
'invoice_address_country_iso' => $shipmentData['invoice_data']['address']['countryISO'],
'invoice_raw_data' => $shipmentData['invoice_data']['invoice_file'],
]);
}
// Instantiate the correct carrier service dynamically for each shipment
$carrierService = CarrierServiceFactory::create($shipment_request->carrier);
try {
$label = $carrierService->genShipLabel($shipment_request);
if ($label) {
$labels[] = $label;
}
} catch (ExternalApiException|LabelPrintException|ShipmentException|GuzzleException|\Exception $e) {
$errors[] = [
'shipmentReference' => $shipmentData['shipmentReference'],
'error' => $e->getMessage(),
];
ShipmentErrorLog::create(
array(
"shipment_request_id" => $shipment_request->id,
"error_message" => $e->getMessage()
)
);
}
}
if (!empty($errors)) {
return response()->json(['errors' => $errors], 400);
}
// Placeholder function to merge labels into a file
$file = $this->mergeLabelsIntoFile($labels);
// return response()->json(['file' => $file], 201);
return response($file, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="labels.pdf"');
}
public function processShipmentsByCarrier(Request $request)
{
Log::channel('expedice')->info('function processShipmentsByCarrier initiated by ' . $request->user()->name, [
'carrier' => $request->get('carrier')
]);
$carrierIds = Carrier::where('carrier_shortname', $request->get('carrier')['shortname'])
->pluck('id')
->toArray();
$shipmentRequests = ShipmentRequest::whereIn('carrier_id', $carrierIds)
->whereDoesntHave('shipment')
->whereDoesntHave('batch')
->get();
$batch = ShipmentRequestBatch::create([
'user_id' => $request->user()->id,
'carrier_master_id' => CarrierMaster::where('shortname', $request->get('carrier')['shortname'])->first()->id,
]);
foreach ($shipmentRequests as $shipment_request) {
// Instantiate the correct carrier service dynamically for each shipment
$carrierService = CarrierServiceFactory::create($shipment_request->carrier);
ShipmentRequestBatchItem::create([
'shipment_request_batch_id' => $batch->id,
'shipment_request_id' => $shipment_request->id,
]);
try {
$label = $carrierService->genShipLabel($shipment_request);
if ($label) {
$labels[] = $label;
}
} catch (ExternalApiException|LabelPrintException|ShipmentException|GuzzleException|\Exception $e) {
$errors[] = [
'shipmentReference' => $shipment_request->shipment_reference,
'error' => $e->getMessage(),
];
ShipmentErrorLog::create(
array(
"shipment_request_id" => $shipment_request->id,
"error_message" => $e->getMessage()
)
);
}
}
try {
if (empty($labels)) {
return response()->json(['errors' => $errors], 400);
}
// Placeholder function to merge labels into a file
$file = $this->mergeLabelsIntoFile($labels);
// return response()->json(['file' => $file], 201);
return response($file, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="labels.pdf"');
} catch (\Exception $e) {
$errors[] = [
'shipmentReference' => 0,
'error' => $e->getMessage(),
];
return response()->json(['errors' => $errors], 400);
}
}
public function validateAndStoreShipment(Request $request)
{
$euCountries = [
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE',
'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK'
];
$base64PdfRule = new Base64Pdf();
foreach ($request->input('shipments', []) as $index => $shipment) {
$carrierId = $shipment['carrier_id'] ?? null;
$countryISO = $shipment['delivery_address']['countryISO'] ?? null;
if (in_array($carrierId, [12, 14, 29, 40968, 40969]) && !in_array($countryISO, $euCountries)) {
$this->validateArray['shipments.*.invoice_data.invoice_file'] = ['required', $base64PdfRule];
$this->validateArray["shipments.*.invoice_data"] = 'required|array';
$this->validateArray["shipments.*.invoice_data.invoiceNumber"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.date"] = 'required|string';
$this->validateArray["shipments.*.invoice_data.currency"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.name"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.companyName"] = 'nullable|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.companyVatNumber"] = 'nullable|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.streetName"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.streetNumber"] = 'required|string|max:10';
$this->validateArray["shipments.*.invoice_data.address.city"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.zip"] = 'required|string|max:20';
$this->validateArray["shipments.*.invoice_data.address.stateISO"] = 'nullable|string|max:10';
$this->validateArray["shipments.*.invoice_data.address.countryISO"] = 'required|string|max:3';
}
}
$validator = Validator::make($request->all(), $this->validateArray);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$validatedData = $validator->validated();
$return_array = [];
$status_code = 200;
foreach ($validatedData['shipments'] as $shipmentData) {
try {
DB::transaction(function () use ($shipmentData, $request, &$return_array) {
// Create shipment request
$shipment_request = ShipmentRequest::create([
'shipment_reference' => $shipmentData['shipmentReference'],
'user_id' => $request->user()->id,
'note' => $shipmentData['note'] ?? null,
'delivery_address_name' => $shipmentData['delivery_address']['name'],
'delivery_address_company_name' => $shipmentData['delivery_address']['companyName'] ?? null,
'delivery_address_street_name' => $shipmentData['delivery_address']['streetName'],
'delivery_address_street_number' => $shipmentData['delivery_address']['streetNumber'],
'delivery_address_address_line_2' => $shipmentData['delivery_address']['addressLine2'] ?? null,
'delivery_address_city' => $shipmentData['delivery_address']['city'],
'delivery_address_zip' => $shipmentData['delivery_address']['zip'],
'delivery_address_state_iso' => $shipmentData['delivery_address']['stateISO'] ?? null,
'delivery_address_country_iso' => $shipmentData['delivery_address']['countryISO'],
'pickup_point_code' => $shipmentData['pickupPointCode'] ?? null,
'pickup_point_address_street_name' => $shipmentData['pickupPointAddress']['streetName'] ?? null,
'pickup_point_address_street_number' => $shipmentData['pickupPointAddress']['streetNumber'] ?? null,
'pickup_point_address_city' => $shipmentData['pickupPointAddress']['city'] ?? null,
'pickup_point_address_zip' => $shipmentData['pickupPointAddress']['zip'] ?? null,
'pickup_point_address_state_iso' => $shipmentData['pickupPointAddress']['stateISO'] ?? null,
'pickup_point_address_country_iso' => $shipmentData['pickupPointAddress']['countryISO'] ?? null,
'contact_email' => $shipmentData['contactEmail'] ?? null,
'contact_telephone' => $shipmentData['contactTelephone'] ?? null,
'cod_value' => $shipmentData['codValue'] ?? null,
'cod_variable_symbol' => $shipmentData['codVariableSymbol'] ?? null,
'shipment_value' => $shipmentData['shipmentValue'],
'currency' => $shipmentData['currency'],
'weight' => $shipmentData['weight'],
'width' => $shipmentData['parcelSize']['width'],
'length' => $shipmentData['parcelSize']['length'],
'height' => $shipmentData['parcelSize']['height'],
'carrier_id' => $shipmentData['carrier_id'],
'allegro_carrier_id' => $shipmentData['allegro_carrier_id'] ?? null,
'package_type' => $shipmentData['packageType'],
'send_again_flag' => $shipmentData['sendAgainFlag'] ?? false,
'allegro_smart' => $shipmentData['allegroSmart'] ?? false,
'postnummer' => $shipmentData['postnummer'] ?? null,
]);
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $shipment_request->id,
'shipment_status_id' => ShipmentStatus::STATUS_CREATED,
)
);
// Create shipment items
foreach ($shipmentData['items'] as $itemData) {
ShipmentRequestItem::create([
'shipment_request_id' => $shipment_request->id,
'name' => $itemData['name'],
'item_note' => $itemData['item_note'] ?? null,
'model_number' => $itemData['model_number'] ?? null,
'HSCode' => $itemData['HSCode'] ?? null,
'price' => $itemData['price'],
'quantity' => $itemData['quantity'],
'originCountry' => $itemData['originCountry'],
'weight' => $itemData['weight'],
'item_id_internal_warehouse' => $itemData['item_id_internal_warehouse'] ?? null,
'item_id_external' => $itemData['item_id_external'] ?? null,
]);
}
if (!empty($shipmentData['invoice_data'])) {
ShipmentRequestInvoice::create([
'shipment_request_id' => $shipment_request->id,
'invoice_number' => $shipmentData['invoice_data']['invoiceNumber'],
'invoice_date' => $shipmentData['invoice_data']['date'],
'invoice_currency' => $shipmentData['invoice_data']['currency'],
'invoice_address_name' => $shipmentData['invoice_data']['address']['name'],
'invoice_address_company_name' => $shipmentData['invoice_data']['address']['companyName'] ?? null,
'invoice_address_company_vat_number' => $shipmentData['invoice_data']['address']['companyVatNumber'] ?? null,
'invoice_address_street_name' => $shipmentData['invoice_data']['address']['streetName'],
'invoice_address_street_number' => $shipmentData['invoice_data']['address']['streetNumber'],
'invoice_address_city' => $shipmentData['invoice_data']['address']['city'],
'invoice_address_zip' => $shipmentData['invoice_data']['address']['zip'],
'invoice_address_state_iso' => $shipmentData['invoice_data']['address']['stateISO'] ?? null,
'invoice_address_country_iso' => $shipmentData['invoice_data']['address']['countryISO'],
'invoice_raw_data' => $shipmentData['invoice_data']['invoice_file'],
]);
}
$return_array[$shipmentData['shipmentReference']] = "Shipment stored sucessfully. Ready for dispatch.";
// if ($shipment_request->user->details->apiCallbackURL && $shipment_request->user->details->callbacks_enabled) {
// $this->callbackService->sendCallback($shipment_request->user->details->apiCallbackURL, [
// 'endpoint' => 'storeShipment',
// 'result' => 'true',
// 'shipment_reference' => $shipmentData['shipmentReference'],
// 'additionalInfo' => null
// ]);
// }
});
} catch (ExternalApiException|LabelPrintException|ShipmentException|GuzzleException|\Exception $e) {
$return_array[$shipmentData['shipmentReference']] = $e->getMessage();
$status_code = 400;
}
}
return response()->json(['response' => $return_array], $status_code);
}
protected function addStickerToLabel(Label $label)
{
try {
$stickerPdfData = ExpediceStickersController::getStickerForLabel($label->shipment->shipmentRequest);
$mainPdfData = $label->label_data;
$client = new Client();
$response = $client->post('http://pdf_manager:5000/combine-pdfs', [
'multipart' => [
[
'name' => 'main_pdf',
'contents' => $mainPdfData,
'filename' => 'main.pdf'
],
[
'name' => 'sticker_pdf',
'contents' => $stickerPdfData,
'filename' => 'sticker.pdf'
],
[
'name' => 'resize',
'contents' => in_array($label->shipment->shipmentRequest->carrier->carrierMaster->shortname, ["ups"]) ? 'true' : 'true'
]
]
]);
return $response->getBody();
}
catch (\Exception $e) {
throw $e;
}
}
// Placeholder function to merge labels into a file
protected function mergeLabelsIntoFile(array $labels)
{
$timestamp = uniqid() . "_" . now()->timestamp;
$outputFilePath = storage_path("app/label_result_" . $timestamp . ".pdf");
$cmd_string = "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/default -dNOPAUSE -dQUIET -dBATCH -dAutoRotatePages=/None -dFIXEDMEDIA -dPDFFitPage -sPAPERSIZE=a4 -sOutputFile=$outputFilePath";
$pdf_files = [];
foreach ($labels as $label) {
$timestamp = uniqid() . "_" . now()->timestamp;
// Log::info($label->shipment->shipmentRequest->id . " - starting sticker to label");
$label_data_final = $this->addStickerToLabel($label);
if (is_null($label_data_final)) {
$label_data_final = $label->label_data;
}
// Log::info($label->shipment->shipmentRequest->id . " - done sticker");
$filename = 'labels-' . $timestamp . '.pdf';
Storage::disk('local')->put($filename, $label_data_final);
$pdf_files[] = Storage::disk('local')->path($filename);
// Log::info($label->shipment->shipmentRequest->id . " - pdf metadata get done");
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $label->shipment->shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_LABEL_CREATED,
)
);
// Log::info($label->shipment->shipmentRequest->id . " - status created");
}
foreach ($pdf_files as $file) {
if (!is_null($file)) {
$cmd_string .= " " . $file;
}
}
Log::info($cmd_string);
// Execute the Ghostscript command to merge PDFs
exec($cmd_string);
// Log::info("command gs done");
// Delete temporary PDF files after merging
foreach ($pdf_files as $file) {
if (file_exists($file)) {
// unlink($file);
}
}
// Log::info("tmp files deleted");
$ret_file = file_get_contents($outputFilePath);
if (file_exists($outputFilePath)) {
unlink($outputFilePath);
}
// Log::info("output final deleted");
return $ret_file;
}
public function labelsList(Request $request)
{
$shipments = ShipmentRequest::with(['carrier'])
->whereDoesntHave('shipment')
->get();
$groupedShipments = $shipments->groupBy('carrier.carrier_shortname')
->sortByDesc(function ($group) {
return $group->count();
});
$result = $groupedShipments->map(function ($items, $key) {
$carrier = $items->first()->carrier;
return [
'carrier_name' => $carrier->carrier_name,
'img' => $carrier->img,
'carrier_shortname' => $carrier->carrier_shortname,
'package_count' => $items->count(),
];
})->values();
// Check if the request is an Inertia request
if ($request->header('X-Inertia')) {
return Inertia::render('Expedice', [
'carriers' => $result,
]);
}
// For API requests, return a JSON response
return response()->json($result);
}
public function reprintLabels(Request $request)
{
$userId = $request->user()->id;
// broadcast(new LabelsProcessed($userId, [
// 'errors' => "test test", // Pass all collected errors
// 'error_code' => 400,
// ]));
// Dispatch the job
ReprintLabelsJob::dispatch(
$request->get('shipmentIds'),
$request->get('batchIds'),
$request->get('send_again', false),
$userId
);
return response()->json(['message' => 'Label creation queued successfully. Please wait....']);
}
public function processViaDifferentCarrier(Request $request)
{
try {
Log::channel('expedice')->info('function processViaDifferentCarrier initiated by ' . $request->user()->name, [
'shipment_request_ids' => $request->get('shipmentIds'),
'carrier_id' => $request->get('carrierId')
]);
$shipment_request = ShipmentRequest::whereIn('id', $request->get('shipmentIds'))->first();
$carrier = Carrier::where('id', $request->get('carrierId'))->first();
$batch = ShipmentRequestBatch::create([
'user_id' => $request->user()->id,
'carrier_master_id' => $carrier->carrier_master_id,
]);
$shipment_request->pickup_point_code = null;
$shipment_request->carrier_id = $carrier->id;
// Save the changes to the database
$shipment_request->save();
// Instantiate the correct carrier service dynamically for each shipment
$carrierService = CarrierServiceFactory::create($carrier);
if (isset($shipment_request->shipment)) {
$res = $carrierService->cancelShipment($shipment_request->shipment);
}
ReprintLabelsJob::dispatch(
$request->get('shipmentIds'),
null,
false,
$request->user()->id
);
return response()->json(['message' => 'Label creation queued successfully. Please wait....']);
}
catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()]);
}
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
class LocationIQController
{
protected $client;
protected $apiKey;
public function __construct()
{
$this->client = new Client();
$this->apiKey = env('LOCATIONIQ_API_KEY'); // Store your API key in the .env file
}
public function validateAddress(Request $request)
{
$address = $request->input('address');
$structured = $request->input('structured');
try {
if($structured) {
$response = $this->client->request('GET', 'https://eu1.locationiq.com/v1/search.php', [
'query' => [
'key' => $this->apiKey,
'street' => "{$address['streetName']} {$address['streetNumber']}",
'city' => $address['city'],
'state' => $address['stateISO'],
'country' => $address['countryISO'],
'postalcode' => $address['zip'],
'format' => 'json',
'addressdetails' => 1,
'normalizeaddress' => 1,
'normalizecity' => 1,
'statecode' => 1,
],
]);
}
else {
$formattedAddress = "{$address['streetName']} {$address['streetNumber']}, {$address['city']}, {$address['zip']}, {$address['stateISO']}, {$address['countryISO']}";
$response = $this->client->request('GET', 'https://eu1.locationiq.com/v1/search.php', [
'query' => [
'key' => $this->apiKey,
'q' => $formattedAddress,
'format' => 'json',
'addressdetails' => 1,
'normalizeaddress' => 1,
'normalizecity' => 1,
'statecode' => 1,
],
]);
}
$responseData = json_decode($response->getBody(), true);
return $responseData;
} catch (RequestException $e) {
// Handle request exceptions, log errors, or return a response
if ($e->hasResponse()) {
$errorResponse = $e->getResponse();
$errorMessage = json_decode($errorResponse->getBody(), true);
return json_decode($errorResponse->getBody(), true);
}
return null; // Handle generic errors
}
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers;
use App\Rules\Base64Pdf;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ParcelSubmitValidationController
{
protected $client;
protected $apiKey;
private $validateArray = [
'shipments' => 'required|array|min:1',
'shipments.*.shipmentReference' => 'required|string|max:255',
'shipments.*.note' => 'nullable|string|max:1000',
'shipments.*.delivery_address' => 'required|array',
'shipments.*.delivery_address.name' => 'required|string|max:255',
'shipments.*.delivery_address.companyName' => 'nullable|string|max:255',
'shipments.*.delivery_address.streetName' => 'required|string|max:255',
'shipments.*.delivery_address.streetNumber' => 'required|string|max:10',
'shipments.*.delivery_address.city' => 'required|string|max:255',
'shipments.*.delivery_address.zip' => 'required|string|max:20',
'shipments.*.delivery_address.stateISO' => 'nullable|string|max:10',
'shipments.*.delivery_address.countryISO' => 'required|string|max:3',
'shipments.*.pickupPointCode' => 'nullable|string|max:255',
'shipments.*.pickupPointName' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.streetName' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.streetNumber' => 'nullable|string|max:10',
'shipments.*.pickupPointAddress.addressLine2' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.city' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.zip' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.stateISO' => 'nullable|string|max:255',
'shipments.*.pickupPointAddress.countryISO' => 'nullable|string|max:255',
'shipments.*.postnummer' => 'nullable|string|max:255',
'shipments.*.contactEmail' => 'nullable|email|max:255',
'shipments.*.contactTelephone' => 'nullable|string|max:20',
'shipments.*.codValue' => 'nullable|numeric',
'shipments.*.codVariableSymbol' => 'nullable|string|max:8',
'shipments.*.shipmentValue' => 'required|numeric',
'shipments.*.currency' => 'required|string|max:3',
'shipments.*.weight' => 'required|numeric',
'shipments.*.items' => 'required|array|min:1',
'shipments.*.items.*.name' => 'required|string|max:255',
'shipments.*.items.*.item_note' => 'nullable|string',
'shipments.*.items.*.HSCode' => 'nullable|string|max:255',
'shipments.*.items.*.model_number' => 'nullable|string|max:255',
'shipments.*.items.*.price' => 'required|numeric',
'shipments.*.items.*.quantity' => 'required|integer',
'shipments.*.items.*.originCountry' => 'required|string|max:3',
'shipments.*.items.*.weight' => 'required|numeric',
'shipments.*.items.*.item_id_internal_warehouse' => 'nullable|numeric',
'shipments.*.items.*.item_id_external' => 'nullable|numeric',
'shipments.*.carrier_id' => 'required|string|max:255',
'shipments.*.allegro_carrier_id' => 'nullable|string|max:255',
'shipments.*.packageType' => 'required|in:ENVELOPE,PACKAGE',
'shipments.*.sendAgainFlag' => 'nullable|boolean',
'shipments.*.allegroSmart' => 'nullable|boolean',
'shipments.*.parcelSize' => 'required|array',
'shipments.*.parcelSize.width' => 'required|string|max:255',
'shipments.*.parcelSize.length' => 'required|string|max:255',
'shipments.*.parcelSize.height' => 'required|string|max:255',
'shipments.*.invoice_data' => 'nullable|array',
'shipments.*.invoice_data.invoiceNumber' => 'nullable|string|max:255',
'shipments.*.invoice_data.date' => 'nullable|string',
'shipments.*.invoice_data.currency' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.name' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.companyName' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.companyVatNumber' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.streetName' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.streetNumber' => 'nullable|string|max:10',
'shipments.*.invoice_data.address.city' => 'nullable|string|max:255',
'shipments.*.invoice_data.address.zip' => 'nullable|string|max:20',
'shipments.*.invoice_data.address.stateISO' => 'nullable|string|max:10',
'shipments.*.invoice_data.address.countryISO' => 'nullable|string|max:3'
];
public function __construct()
{
}
public function parcelDataValidate(Request $request)
{
$euCountries = [
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE',
'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK'
];
$base64PdfRule = new Base64Pdf();
foreach ($request->input('shipments', []) as $index => $shipment) {
$carrierId = $shipment['carrier_id'] ?? null;
$countryISO = $shipment['delivery_address']['countryISO'] ?? null;
if (in_array($carrierId, [12, 14, 29, 40968, 40969]) && !in_array($countryISO, $euCountries)) {
$this->validateArray['shipments.*.invoice_data.invoice_file'] = ['required', $base64PdfRule];
$this->validateArray["shipments.*.invoice_data"] = 'required|array';
$this->validateArray["shipments.*.invoice_data.invoiceNumber"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.date"] = 'required|string';
$this->validateArray["shipments.*.invoice_data.currency"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.name"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.companyName"] = 'nullable|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.companyVatNumber"] = 'nullable|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.streetName"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.streetNumber"] = 'required|string|max:10';
$this->validateArray["shipments.*.invoice_data.address.city"] = 'required|string|max:255';
$this->validateArray["shipments.*.invoice_data.address.zip"] = 'required|string|max:20';
$this->validateArray["shipments.*.invoice_data.address.stateISO"] = 'required|string|max:10';
$this->validateArray["shipments.*.invoice_data.address.countryISO"] = 'required|string|max:3';
}
}
$validator = Validator::make($request->all(), $this->validateArray);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
return response()->json([
'success' => true
], 200);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use App\Models\Carrier;
use App\Models\CarrierBasePricing;
use App\Models\CarrierBasePricing2Weight;
use App\Models\PickupPoint;
use App\Models\ShipmentRequest;
use App\Models\User;
use App\Models\UserCarrierPricing;
use App\Models\UserCarrierPricing2Weight;
use App\Models\UserDetails;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PickupPointController extends Controller {
function checkAlternativePickupPoints(Request $request) {
$shipment = ShipmentRequest::where('id', $request->get('shipmentId'))->first();
$pickupPoints = PickupPoint::getNearbyPickupPoints($shipment->pickup_point_code, $shipment->carrier->id)
->load('carrier.carrierMaster');
foreach ($pickupPoints as $pickupPoint) {
// Find the shipment price for each pickup point
$shipment_price_object = $shipment->carrier->userCarrierPricings()
->whereHas('user', function ($query) use ($shipment) {
$query->where('id', $shipment->user->id);
})->first();
if (!$shipment_price_object) {
$shipment_price_object = $shipment->carrier->basePricing()->first();
$user_carrier_grp_uplift = $shipment->user->userCarrierPricingGroup->carrierPricingGroup->base_ratio;
$shipment_price = round(
$shipment_price_object->getPriceForWeight((float)$shipment->weight) +
($shipment_price_object->getPriceForWeight((float)$shipment->weight) * ($user_carrier_grp_uplift / 100)),
0
);
} else {
$shipment_price = $shipment_price_object->getPriceForWeight((float)$shipment->weight);
}
// Add the calculated shipment price to the pickup point
$pickupPoint->shipment_price = $shipment_price;
}
return response()->json($pickupPoints);
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
$user = $request->user()->load('details');
$token = $user->tokens->first()->token ?? null;
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $user instanceof MustVerifyEmail,
'user' => $user,
'status' => session('status'),
'apiToken' => $token,
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Update the user's profile information.
*/
public function updateApiPreferences(Request $request)
{
if(isset($request->user()->details)) {
if($request->get('apiCallbackURL') !== null) {
$request->user()->details->apiCallbackURL = $request->get('apiCallbackURL');
}
if($request->get('apiCallbacksAllowed') !== null) {
$request->user()->details->callbacks_enabled = $request->get('apiCallbacksAllowed');
}
$request->user()->details->save();
}
return response()->json([
'user' => $request->user()
]);
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
public function logout(Request $request): RedirectResponse
{
Auth::guard("web")->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
<?php
// app/Http/Controllers/ShipmentController.php
namespace App\Http\Controllers;
use App\Factories\CarrierServiceFactory;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentStatus;
use App\Models\ShipmentStatusHistory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class StatusController extends Controller
{
public function statusList()
{
$statuses = ShipmentStatus::all();
// For API requests, return a JSON response
return response()->json($statuses);
}
public function statusFilterList()
{
$statuses = ShipmentStatus::all()->pluck('name');
// For API requests, return a JSON response
return response()->json($statuses);
}
public function addStatus(Request $request)
{
if(is_array($request->get('shipment_request_id'))) {
foreach($request->get('shipment_request_id') as $sreq_id) {
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $sreq_id,
'shipment_status_id' => $request->get('statusId'),
)
);
}
}
else {
if($request->get('statusId') === ShipmentStatus::STATUS_CANCELED) {
return $this->cancelShipment($request);
}
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $request->get('shipment_request_id'),
'shipment_status_id' => $request->get('statusId'),
)
);
}
// For API requests, return a JSON response
return response()->json(true);
}
public function requeueShipment(Request $request)
{
try {
$user = $request->user();
if ($user->hasRole('customer')) {
$shipmentRequest = ShipmentRequest::where('shipment_reference', $request->get('shipment_reference'))
->where('user_id', $user->id)
->first();
}
else {
$shipmentRequest = ShipmentRequest::where('shipment_reference', $request->get('shipment_reference'))
->first();
}
if(!is_null($shipmentRequest->shipment)) {
return response()->json(['result' => false, 'error' => "Label already exists, cannot requeue shipment."]);
}
if (!is_null($shipmentRequest->batch)) {
ShipmentRequestBatchItem::where('shipment_request_id', $shipmentRequest->id)->delete();
}
if($shipmentRequest->currentShipmentStatus->shipment_status_id === ShipmentStatus::STATUS_IS_REPAIR) {
$shipmentRequest->send_again_flag = true;
$shipmentRequest->save();
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_PENDING,
)
);
}
else {
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_PENDING,
)
);
}
// For API requests, return a JSON response
return response()->json(['result' => true]);
}
catch (\Exception $e) {
return response()->json(['result' => false, 'error' => $e->getMessage()]);
}
}
public function cancelShipment(Request $request)
{
try {
if(is_null($request->get('shipment_reference')) && !is_null($request->get('shipment_request_id'))) {
$shipmentRequest = ShipmentRequest::where('id', $request->get('shipment_request_id'))
->first();
}
else {
$user = $request->user();
if ($user->hasRole('customer')) {
$shipmentRequest = ShipmentRequest::where('shipment_reference', $request->get('shipment_reference'))
->where('user_id', $user->id)
->first();
}
else {
$shipmentRequest = ShipmentRequest::where('shipment_reference', $request->get('shipment_reference'))
->first();
}
}
Log::channel('expedice')->info("Shipment cancel initiated by " . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentRequest->id
]);
if (!is_null($shipmentRequest->batch)) {
ShipmentRequestBatchItem::where('shipment_request_id', $shipmentRequest->id)->delete();
}
if(isset($shipmentRequest->shipment)) {
$carrierService = CarrierServiceFactory::create($shipmentRequest->carrier);
$res = $carrierService->cancelShipment($shipmentRequest->shipment);
}
// $test = $carrierService->testCheckShipment($shipmentRequest->shipment);
ShipmentStatusHistory::create(
array(
'shipment_request_id' => $shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_CANCELED,
)
);
if(!$res) {
Log::channel('expedice')->info("Shipment cancel (automatic) unsuccessful - " . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentRequest->id
]);
return response()->json(['result' => true, "message" => "Unable to cancel shipment in the carrier system, please cancel the shipment manually."]);
}
else {
optional($shipmentRequest->shipment?->label)->delete();
optional($shipmentRequest->shipment)->delete();
Log::channel('expedice')->info("Shipment cancel (automatic) OK - " . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentRequest->id
]);
return response()->json(['result' => true]);
}
}
catch (\Exception $e) {
return response()->json(['result' => false, 'error' => $e->getMessage()]);
}
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\SortStockWithdrawal;
use App\Jobs\UpdateStockWithdrawalCache;
use App\Models\ItemsStorageRemoval;
use App\Models\ShipmentRequestItem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class StockWithdrawalController extends Controller
{
public function markProcessed(Request $request) {
Log::channel('storage_management')->info("Withdraw attempt from stock by " . $request->user()->name, [
'request' => $request->all()
]);
$stock_items_ids = $request->post('selectedStock');
ItemsStorageRemoval::whereIn('shipment_request_item_id', $stock_items_ids)->update(['entry_processed' => true]);
Log::channel('storage_management')->info("Items withdrawn from stock by " . $request->user()->name, [
'shipment_request_items_ids' => $stock_items_ids
]);
return response()->json(['message' => 'Stock marked as processed']);
}
public function start(): JsonResponse
{
// Dispatch the job manually
UpdateStockWithdrawalCache::dispatch()->onQueue('storage_management');
return response()->json(['message' => 'Stock withdrawal process started.']);
}
public function sort(): JsonResponse
{
// Dispatch the job manually
SortStockWithdrawal::dispatch()->onQueue('storage_management');
return response()->json(['message' => 'Stock sorting process started.']);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Models\Carrier;
use App\Models\CarrierMaster;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentRequestBatchItem;
use App\Models\ShipmentRequestInvoice;
use App\Models\ShipmentRequestItem;
use App\Models\User;
use App\Models\UserDetails;
use App\Rules\Base64Pdf;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use App\Models\Shipment;
use App\Factories\CarrierServiceFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class UserController extends Controller {
public function userDetail($id) {
$userData = User::with([
'details.carrierPricings.carrier',
'details.carrierPricings.pricingWeights',
'details.carrierPricings.extraFees.extraFeeType'
])->find($id);
return Inertia::render('Admin/UserDetails', [
'user' => $userData,
]);
}
public function update(Request $request)
{
// Validate the request data
$validatedData = $request->validate([
'id' => 'required|integer',
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'apiCallbackURL' => 'nullable|url|max:255',
'callbacks_enabled' => 'required|boolean',
]);
// Find the user by its ID
$user = User::findOrFail($validatedData['id']);
// Update the User model
$user->update([
'name' => $validatedData['name'],
'email' => $validatedData['email'],
]);
// Update the UserDetails model
$userDetails = UserDetails::where('user_id', $user->id)->firstOrFail();
$userDetails->update([
'apiCallbackURL' => $validatedData['apiCallbackURL'],
'callbacks_enabled' => $validatedData['callbacks_enabled'],
]);
// Return a success response
return response()->json(['message' => 'User and UserDetails updated successfully', 'user' => $user, 'userDetails' => $userDetails]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers;
use App\Models\Carrier;
use App\Models\User;
use App\Models\UserCarrierPricing;
use App\Models\UserCarrierPricing2Weight;
use App\Models\UserDetails;
use Illuminate\Http\Request;
use Inertia\Inertia;
class UserPricingController extends Controller {
public function updatePricing(Request $request, $id)
{
// Validate the request data
$request->validate([
'cod_price' => 'required|numeric|min:0', // example validation rule
]);
// Find the record by ID and update the cod_price
$record = UserCarrierPricing::findOrFail($id);
$record->update([
'cod_price' => $request->get('cod_price'),
]);
// Return a success response
return response()->json([
'success' => true,
]);
}
public function addPricing(Request $request) {
UserCarrierPricing::create([
'user_id' => $request->get('user_id'),
'carrier_id' => $request->get('carrier_id'),
'cod_price' => $request->get('cod_price'),
]);
return response()->json([
'success' => true,
]);
}
public function updatePricingWeight(Request $request, $id)
{
$record = UserCarrierPricing2Weight::findOrFail($id);
$record->update([
'weight_max' => $request->get('weight_max'),
'shipping_price' => $request->get('shipping_price'),
]);
// Return a success response
return response()->json([
'success' => true,
]);
}
public function deletePricingWeight($id)
{
// Retrieve the record by its ID
$record = UserCarrierPricing2Weight::findOrFail($id);
// Delete the record from the database
$record->delete();
// Return a success response
return response()->json([
'success' => true,
]);
}
public function addPricingWeight(Request $request) {
UserCarrierPricing2Weight::create([
'user_carrier_pricing_id' => $request->get('user_carrier_pricing_id'),
'weight_max' => $request->get('weight_max'),
'shipping_price' => $request->get('shipping_price'),
]);
return response()->json([
'success' => true,
]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class ApiRequestLogger
{
public function handle($request, Closure $next)
{
// Log the request data to the custom log file
Log::channel('api_requests')->info('API Request', [
'method' => $request->getMethod(),
'url' => $request->fullUrl(),
'headers' => $request->headers->all(),
'body' => $request->all(),
]);
$response = $next($request);
// Log the response data to the custom log file
Log::channel('api_requests')->info('API Response', [
'status' => $response->getStatusCode(),
'body' => $response->getContent(),
]);
return $response;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): string|null
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
];
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Interfaces;
use App\Models\Label;use App\Models\Shipment;
use App\Models\ShipmentRequest;
interface CarrierInterface {
public function createShipment(ShipmentRequest $shipment_request) : Shipment;
function prepareData(ShipmentRequest $shipment_request): array;
function savePackageData(ShipmentRequest $shipment_request, mixed $res) : Shipment;
public function getLabel(Shipment $shipment) : string|null;
public function genShipLabel(ShipmentRequest $shipment_request) : Label;
}
?>

View File

@ -0,0 +1,20 @@
<?php
// app/Jobs/DeleteExpiredFiles.php
namespace App\Jobs;
use App\Models\GeneratedFile;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class DeleteExpiredFiles implements ShouldQueue
{
public function __invoke()
{
// Delete records where the lifetime has passed
GeneratedFile::where('lifetime', '<=', now())->delete();
Log::channel('jobs')->info('Expired files deleted successfully.');
}
}

View File

@ -0,0 +1,24 @@
<?php
// app/Jobs/DeleteExpiredFiles.php
namespace App\Jobs;
use App\Models\GeneratedFile;
use App\Models\Label;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class DeleteTemporaryLabels implements ShouldQueue
{
public function __invoke()
{
$deletedCount = Label::where('temporary', true)
->where('created_at', '<', now()->subHours(48))
->delete();
Log::channel('jobs')->info($deletedCount . ' temporary labels older than 48 hours have been deleted.');
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Jobs;
use App\Models\Shipment;
use App\Models\ShipmentTrackingStatus;
use App\Models\ShipmentTrackingStatusHistory;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class FetchTrackingStatus implements ShouldQueue
{
public function __invoke()
{
Log::channel('jobs')->info('----------------------Tracking status fetch STARTED------------------------------');
Shipment::where('created_at', '>=', Carbon::create(2024, 11, 28))
->whereDoesntHave('trackingStatusHistory', function ($query) {
$query->where('final_status', true);
})
->chunk(100, function ($shipments) {
foreach ($shipments as $shipment) {
// Dispatch a sub-job for each shipment
FetchTrackingStatusSubJob::dispatch($shipment)->onQueue('fetch_tracking');
Log::channel('jobs')->info('Subjob dispatched', [
'shipment_id' => $shipment->id,
]);
}
});
$shipment_results = ShipmentTrackingStatusHistory::whereIn('shipment_tracking_status_id', [
ShipmentTrackingStatus::DELIVERED,
ShipmentTrackingStatus::CANCELED,
ShipmentTrackingStatus::RETURNED,
])
->where('final_status', false)
->get();
foreach ($shipment_results as $result) {
$result->final_status = true;
$result->save(); // Triggers the saving event
}
Log::channel('jobs')->info('----------------------Tracking status fetch FINISHED------------------------------');
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Factories\CarrierServiceFactory;
use App\Models\Shipment;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class FetchTrackingStatusSubJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $shipment;
public function __construct(Shipment $shipment)
{
$this->shipment = $shipment;
}
public function handle()
{
try {
// Create carrier service and process the tracking history
$carrierService = CarrierServiceFactory::create($this->shipment->shipmentRequest->carrier);
$status_fetch_result = $carrierService->processTrackingHistory($this->shipment);
Log::channel('jobs')->info('Tracking status fetch', [
'shipment_id' => $this->shipment->id,
'new_status_count' => $status_fetch_result,
]);
} catch (\Exception $exception) {
Log::channel('jobs')->error($exception->getMessage(), [
'shipment_id' => $this->shipment->id,
]);
}
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace App\Jobs;
use App\Http\Controllers\ExpediceStickersController;
use App\Models\ActionLog;
use App\Models\GeneratedFile;
use App\Models\Label;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentStatus;
use App\Models\ShipmentStatusHistory;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class MergeLabelsIntoFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $labelIds;
protected $id;
protected $origin;
protected $author_id;
/**
* Create a new job instance.
*
* @param array $labels
*/
public function __construct(string $origin, string $id, array $labelIds, int $author_id)
{
$this->labelIds = $labelIds; // Store only the IDs
$this->id = $id;
$this->origin = $origin;
$this->author_id = $author_id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// Retrieve the Label instances using their IDs
$labels = Label::whereIn('id', $this->labelIds)->get();
$timestamp = uniqid() . "_" . now()->timestamp;
$outputFilePath = storage_path("app/label_result_" . $timestamp . ".pdf");
$cmd_string = "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/default -dNOPAUSE -dQUIET -dBATCH -dAutoRotatePages=/None -dFIXEDMEDIA -dPDFFitPage -sPAPERSIZE=a4 -sOutputFile=$outputFilePath";
$pdf_files = [];
try {
Log::channel('jobs')->info('MergeLabelsIntoFileJob started...');
foreach ($labels as $label) {
$timestamp = uniqid() . "_" . now()->timestamp;
// Log::info("processing - " . $label->id);
$label_data_final = $this->addSticker($label);
if (is_null($label_data_final)) {
$label_data_final = $label->label_data;
}
$filename = 'labels-' . $timestamp . '.pdf';
Storage::disk('local')->put($filename, $label_data_final);
$pdf_files[] = Storage::disk('local')->path($filename);
// Record shipment status
ShipmentStatusHistory::create([
'shipment_request_id' => $label->shipment->shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_LABEL_CREATED,
]);
}
}
catch (\Exception $e) {
Log::error('MergeLabelsIntoFileJob failed label creation', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
foreach ($pdf_files as $file) {
if (!is_null($file)) {
$cmd_string .= " " . $file;
}
}
Log::channel('jobs')->info('MergeLabelsIntoFileJob - gs command ---- ' . $cmd_string);
exec($cmd_string);
Log::channel('jobs')->info('MergeLabelsIntoFileJob --- Tmp files delete start');
// Delete temporary PDF files after merging
foreach ($pdf_files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
Log::channel('jobs')->info('MergeLabelsIntoFileJob --- Tmp files deleted');
$ret_file = file_get_contents($outputFilePath);
if (file_exists($outputFilePath)) {
unlink($outputFilePath);
}
// Save file to the database
GeneratedFile::create([
'file_data' => base64_encode($ret_file),
'user_id' => $this->author_id ?? 1,
'filename' => $this->origin . "_" . $this->id,
'associated_id' => $this->origin === "batch" ? $this->id : null,
'lifetime' => now()->addHours(12), // Example lifetime, adjust as needed
'type' => $this->origin,
]);
ActionLog::create([
'name' => "labels_printed",
'description' => "Labels printed (file generated) for batch - '{$this->id}'",
'user_id' => $this->author_id ?? 1
]);
Label::whereIn('id', $this->labelIds)
->where('temporary', true)
->delete();
ActionLog::create([
'name' => "labels_deleted_tmp",
'description' => "Temporary labels deleted for batch - '{$this->id}'",
'user_id' => $this->author_id ?? 1
]);
if($this->origin === "batch") {
$batch = ShipmentRequestBatch::where('id', $this->id)->first();
if ($batch) {
$batch->update(['printed' => true]);
}
}
}
/**
* Mockup method for adding a sticker to label
*
* @param object $label
* @return string|null
*/
private function addSticker(Label $label) {
try {
$stickerPdfData = ExpediceStickersController::getStickerForLabel($label->shipment->shipmentRequest);
$mainPdfData = $label->label_data;
$client = new Client();
// Define the command to execute
$response = $client->post('http://pdf_manager:5000/combine-pdfs', [
'multipart' => [
[
'name' => 'main_pdf',
'contents' => $mainPdfData,
'filename' => 'main.pdf'
],
[
'name' => 'sticker_pdf',
'contents' => $stickerPdfData,
'filename' => 'sticker.pdf'
],
[
'name' => 'resize',
'contents' => in_array($label->shipment->shipmentRequest->carrier->carrierMaster->shortname, ["ups"]) ? 'true' : 'true'
]
]
]);
return $response->getBody();
} catch (\Exception $e) {
throw $e;
}
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Jobs;
use App\Factories\CarrierServiceFactory;
use App\Exceptions\ShipmentException;
use App\Exceptions\ExternalApiException;
use App\Models\ShipmentErrorLog;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessShipmentsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $shipments;
// public $tries = 5; // Number of retry attempts
public function __construct(array $shipments)
{
$this->shipments = $shipments;
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
// public function retryUntil()
// {
// return now()->addMinutes(1); // Customize the retry time limit
// }
public function handle()
{
foreach ($this->shipments as $shipmentData) {
try {
$carrierService = CarrierServiceFactory::create($shipmentData['carrier']);
$label = $carrierService->genShipLabel($shipmentData);
if ($label) {
// Store the label in the database or any other persistent storage
}
} catch (ShipmentException $e) {
$this->storeError($shipmentData, $e->getMessage());
throw $e;
} catch (ExternalApiException $e) {
$this->storeError($shipmentData, $e->getMessage());
throw $e;
} catch (\Exception $e) {
$this->storeError($shipmentData, $e->getMessage());
throw $e;
}
}
}
protected function storeError($shipmentData, $errorMessage)
{
ShipmentErrorLog::create([
'user_id' => 1,
'shipment_reference' => 123,
'error_message' => $errorMessage,
]);
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use function App\Helpers\getProductID;
class ProcessStockWithdrawalItem implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
protected $items;
/**
* Create a new job instance.
*
* @param object $items
*/
public function __construct($items)
{
$this->items = $items;
}
public function handle()
{
$stock_first_array = [];
$totalItems = $this->items->count();
$processedCount = 0;
$lastLoggedPercentage = 0;
foreach ($this->items as $item) {
try {
$processedCount++;
$groupedData = [];
// Get eshop_id using getProductID function
$eshop_product_id = getProductID($item->item_id_internal_warehouse);
if ($eshop_product_id) {
// Group data by eshop_id
if (!isset($groupedData[$eshop_product_id])) {
$groupedData[$eshop_product_id] = [
'eshop_id' => $eshop_product_id,
'mapping_id' => $item->item_id_internal_warehouse,
'total_quantity' => 0,
'name' => $item->name,
'model_number' => $item->model_number,
'shipment_request_item_id' => $item->id,
'stock_positions' => [],
];
}
// Accumulate total quantity
$groupedData[$eshop_product_id]['total_quantity'] += $item->quantity;
} else {
Log::channel('storage_management')->warning("Item ID (eshop) not found for item", ['item' => $item]);
continue;
}
// Fetch warehouse data for each grouped eshop_id
foreach ($groupedData as $eshop_product_id => &$data) {
$url = "https://sklad.dalkove-ovladace.cz/api/warehouse/2/{$eshop_product_id}?includeOutOfStock=true";
$json = file_get_contents($url);
$warehouseObj = json_decode($json);
$stockData = [];
foreach ($warehouseObj as $warehouseItem) {
foreach ($warehouseItem->warehouseItems as $position) {
$arr = [
'location' => $position->location,
'count' => $position->count,
'physicalItemName' => $warehouseItem->physicalItemName,
'modelNumber' => $warehouseItem->modelNumber,
];
$stockData[$warehouseItem->physicalItemId][] = $arr;
}
}
$data['stock_positions'] = $stockData;
if (!isset($stock_first_array[$warehouseItem->physicalItemId])) {
$stock_first_array[$warehouseItem->physicalItemId] = [
"to_be_withdrawn" => 0,
"name" => $warehouseItem->physicalItemName,
"model" => $warehouseItem->modelNumber,
"physicalItemId" => $warehouseItem->physicalItemId,
"mapping_breakdown" => []
];
}
$stock_first_array[$warehouseItem->physicalItemId]['to_be_withdrawn'] += $data['total_quantity'];
if (!isset($stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown'][$data['mapping_id']])) {
$stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown'][$data['mapping_id']] = [
'to_be_withdrawn' => 0,
'name' => $data['name'],
'model' => $data['model_number'],
'mapping_id' => $data['mapping_id'],
'shipment_request_item_ids' => []
];
}
$stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown'][$data['mapping_id']]['to_be_withdrawn'] += $data['total_quantity'];
$stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown'][$data['mapping_id']]['shipment_request_item_ids'][] = $data['shipment_request_item_id'];
$stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown'] = array_values($stock_first_array[$warehouseItem->physicalItemId]['mapping_breakdown']);
}
$currentPercentage = intval(($processedCount / $totalItems) * 100);
if ($currentPercentage >= $lastLoggedPercentage + 5) {
$lastLoggedPercentage = $currentPercentage;
Log::channel('storage_management')->info("Progress: {$currentPercentage}% completed.");
}
// Log::channel('storage_management')->info("Processed item successfully.", ['item' => $this->item]);
} catch (\Exception $e) {
Log::channel('storage_management')->error("Failed to process item.", [
'item' => $item,
'url' => $url ?? null,
'json_sklad' => $json ?? null,
'error' => $e->getMessage(),
]);
}
}
Cache::put('stock_withdrawal_data', $stock_first_array, now()->addDay()); // Cache for a day
Cache::put('stock_withdrawal_data_last_update', now()->format('d-m-y H:i:s'), now()->addDay());
Log::channel('storage_management')->info("Stock withdrawal items processed...", [
'timestamp' => now()->format('d-m-y H:i:s'),
]);
SortStockWithdrawal::dispatch()->onQueue('storage_management');
}
}

View File

@ -0,0 +1,384 @@
<?php
namespace App\Jobs;
use App\Events\LabelsProcessed;
use App\Exceptions\ExternalApiException;
use App\Exceptions\LabelPrintException;
use App\Exceptions\ShipmentException;
use App\Factories\CarrierServiceFactory;
use App\Http\Controllers\ExpediceStickersController;
use App\Models\CarrierExtraFeeTypes;
use App\Models\GeneratedFile;
use App\Models\Label;
use App\Models\ShipmentErrorLog;
use App\Models\ShipmentProcessedBaseFee;
use App\Models\ShipmentProcessedExtraFee;
use App\Models\ShipmentRequest;
use App\Models\ShipmentRequestBatch;
use App\Models\ShipmentStatus;
use App\Models\ShipmentStatusHistory;
use App\Models\User;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ReprintLabelsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $shipmentIds;
protected $batchIds;
protected $sendAgain;
protected $userId;
protected $origin;
protected $ids_text;
/**
* Create a new job instance.
*
* @param array|null $shipmentIds
* @param array|null $batchIds
* @param bool $sendAgain
* @param int|null $userId
*/
public function __construct($shipmentIds = null, $batchIds = null, $sendAgain = false, $userId = null)
{
$this->shipmentIds = $shipmentIds;
$this->batchIds = $batchIds;
$this->sendAgain = $sendAgain;
$this->userId = $userId;
}
public function handle()
{
$user = User::find($this->userId);
Log::channel('expedice')->info('ReprintLabelsJob started by - ' . $user->name, [
'shipment_request_ids' => $this->shipmentIds ?? null,
'batch_ids' => $this->batchIds ?? null,
]);
$this->origin = "";
if ($this->batchIds !== null) {
$this->origin = "batch";
$batchIds = $this->batchIds;
$shipmentRequests = ShipmentRequest::with(['items', 'shipment'])
->whereHas('shipment')
->whereHas('batch', function ($query) use ($batchIds) {
$query->whereIn('id', $batchIds);
})
->get();
} elseif ($this->shipmentIds !== null) {
$this->origin = "shipments";
$shipmentRequests = ShipmentRequest::with(['items', 'shipment'])
->whereHas('shipment')
->whereIn('id', $this->shipmentIds)
->get();
}
foreach ($shipmentRequests as $shipment_request) {
// Instantiate the correct carrier service dynamically for each shipment
$carrierService = CarrierServiceFactory::create($shipment_request->carrier);
try {
$label = $carrierService->reprintLabel($shipment_request->shipment);
// $label = Label::storeEntry($carrierService->getLabel($shipment_request->shipment), $shipment_request->shipment);
if ($label) {
$labels[] = $label;
}
} catch (ExternalApiException|LabelPrintException|ShipmentException|GuzzleException|\Exception $e) {
$errors[] = [
'shipmentReference' => $shipment_request->shipment_reference,
'error' => $e->getMessage(),
];
ShipmentErrorLog::create(
array(
"shipment_request_id" => $shipment_request->id,
"error_message" => $e->getMessage()
)
);
}
}
if ($this->batchIds !== null) {
$this->origin = "batch";
$batchIds = $this->batchIds;
$shipmentRequests = ShipmentRequest::with('items')
->whereDoesntHave('shipment')
->whereHas('batch', function ($query) use ($batchIds) {
$query->whereIn('id', $batchIds);
})
->get();
} elseif ($this->shipmentIds !== null) {
$this->origin = "shipments";
$shipmentRequests = ShipmentRequest::with('items')
->whereDoesntHave('shipment')
->whereIn('id', $this->shipmentIds)
->get();
}
foreach ($shipmentRequests as $shipment_request) {
// Instantiate the correct carrier service dynamically for each shipment
$carrierService = CarrierServiceFactory::create($shipment_request->carrier);
try {
$label = $carrierService->genShipLabel($shipment_request);
if ($label) {
$labels[] = $label;
}
if ($this->sendAgain === true) {
$shipment = $shipment_request->shipment;
if (!$shipment) {
continue;
}
$shipment_price_object = $shipment_request->carrier->userCarrierPricings()->whereHas('user', function ($query) use ($shipment_request) {
$query->where('id', $shipment_request->user->id);
})->first();
if (!$shipment_price_object) {
$shipment_price_object = $shipment_request->carrier->basePricing()->first();
$user_carrier_grp_uplift = $shipment_request->user->userCarrierPricingGroup->carrierPricingGroup->base_ratio;
$shipment_price = round($shipment_price_object->getPriceForWeight((float)$shipment_request->weight) + ($shipment_price_object->getPriceForWeight((float)$shipment_request->weight) * ($user_carrier_grp_uplift / 100)), 0);
} else {
$shipment_price = $shipment_price_object->getPriceForWeight((float)$shipment_request->weight);
}
// Prepare the base fees and extra fees
$baseFeesData = [
'shipment_id' => $shipment->id,
'base_fee_type' => 'shipping',
'base_fee_value' => $shipment_price,
'base_fee_desc' => 'Shipping price',
];
ShipmentProcessedBaseFee::create($baseFeesData);
if (isset($shipment_request->cod_value) && is_numeric($shipment_request->cod_value) && $shipment_request->cod_value >= 0) {
$cod_extra_fee_type = CarrierExtraFeeTypes::where('name', 'COD')->first();
$extraFeesData = [
'shipment_id' => $shipment->id,
'extra_fee_type_id' => $cod_extra_fee_type->id,
'extra_fee_type_value' => $shipment_price_object->extraFees()->where('carrier_extra_fee_type_id', $cod_extra_fee_type->id)->value('carrier_extra_fee_value'),
'extra_fee_desc' => "COD fee",
];
ShipmentProcessedExtraFee::create($extraFeesData);
}
}
} catch (ExternalApiException|LabelPrintException|ShipmentException|GuzzleException|\Exception $e) {
$errors[] = [
'shipmentReference' => $shipment_request->shipment_reference,
'error' => $e->getMessage(),
];
ShipmentErrorLog::create(
array(
"shipment_request_id" => $shipment_request->id,
"error_message" => $e->getMessage()
)
);
}
}
if (!empty($errors)) {
broadcast(new LabelsProcessed($this->userId, [
'errors' => array_unshift($errors, 'ALERT - Some shipments [ ' . sizeof($errors) . '/' . $shipmentRequests->count() . ' ] had errors --- '),
'error_code' => 400,
]));
// Throw an exception to mark the job as failed
// throw new \Exception('Job failed due to errors: ' . json_encode($errors));
}
if (isset($labels)) {
try {
if ($this->origin === "batch") {
$this->ids_text = implode("_", $this->batchIds);
} else {
$this->ids_text = implode("_", $this->shipmentIds);
}
$file_id = $this->mergeLabelsIntoFile($labels);
broadcast(new LabelsProcessed($this->userId, [
'message' => 'Labels created sucessfully',
'file_id' => $file_id,
'filename' => $this->ids_text,
'origin' => $this->origin,
]));
return true;
} catch (\Exception $e) {
broadcast(new LabelsProcessed($this->userId, [
'error' => $e->getMessage(),
'error_code' => 400
]));
// Throw an exception to mark the job as failed
throw new \Exception('Job failed due to Exception: ' . $e->getMessage());
}
} else {
broadcast(new LabelsProcessed($this->userId, [
'errors' => $errors,
'error_code' => 400
]));
return false;
}
}
private function mergeLabelsIntoFile($labels)
{
// Retrieve the Label instances using their IDs
$timestamp = uniqid() . "_" . now()->timestamp;
$outputFilePath = storage_path("app/label_result_" . $timestamp . ".pdf");
$cmd_string = "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/default -dNOPAUSE -dQUIET -dBATCH -dAutoRotatePages=/None -dFIXEDMEDIA -dPDFFitPage -sPAPERSIZE=a4 -sOutputFile=$outputFilePath";
$pdf_files = [];
try {
Log::channel('expedice')->info('MergeLabelsIntoFile inside ReprintLabelsJob started...');
foreach ($labels as $label) {
$timestamp = uniqid() . "_" . now()->timestamp;
// Log::info("processing - " . $label->id);
$label_data_final = $this->addSticker($label);
if (is_null($label_data_final)) {
$label_data_final = $label->label_data;
}
$filename = 'labels-' . $timestamp . '.pdf';
Storage::disk('local')->put($filename, $label_data_final);
$pdf_files[] = Storage::disk('local')->path($filename);
// Record shipment status
ShipmentStatusHistory::create([
'shipment_request_id' => $label->shipment->shipmentRequest->id,
'shipment_status_id' => ShipmentStatus::STATUS_LABEL_CREATED,
]);
}
} catch (\Exception $e) {
Log::channel('expedice')->error('MergeLabelsIntoFile function in ReprintLabelsJob failed label creation', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
foreach ($pdf_files as $file) {
if (!is_null($file)) {
$cmd_string .= " " . $file;
}
}
Log::channel('jobs')->info('MergeLabelsIntoFile function in ReprintLabelsJob - gs command ---- ' . $cmd_string);
exec($cmd_string);
Log::channel('jobs')->info('MergeLabelsIntoFile function in ReprintLabelsJob --- Tmp files delete start');
// Delete temporary PDF files after merging
foreach ($pdf_files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
Log::channel('jobs')->info('MergeLabelsIntoFile function in ReprintLabelsJob --- Tmp files deleted');
$ret_file = file_get_contents($outputFilePath);
if (file_exists($outputFilePath)) {
unlink($outputFilePath);
}
// Save file to the database
$newFile = GeneratedFile::create([
'file_data' => base64_encode($ret_file),
'user_id' => $this->userId ?? 1,
'filename' => $this->origin . "_" . $this->ids_text,
'associated_id' => $this->origin === "batch" ? $this->ids_text : null,
'lifetime' => now()->addHours(12), // Example lifetime, adjust as needed
'type' => $this->origin,
]);
Label::whereIn('id', collect($labels)->pluck('id')->toArray())
->where('temporary', true)
->delete();
if ($this->origin === "batch") {
$batch = ShipmentRequestBatch::where('id', $this->ids_text)->first();
if ($batch) {
$batch->update(['printed' => true]);
}
}
return $newFile->id;
}
private function addSticker(Label $label) {
try {
$stickerPdfData = ExpediceStickersController::getStickerForLabel($label->shipment->shipmentRequest);
$mainPdfData = $label->label_data;
$client = new Client();
// Define the command to execute
$response = $client->post('http://pdf_manager:5000/combine-pdfs', [
'multipart' => [
[
'name' => 'main_pdf',
'contents' => $mainPdfData,
'filename' => 'main.pdf'
],
[
'name' => 'sticker_pdf',
'contents' => $stickerPdfData,
'filename' => 'sticker.pdf'
],
[
'name' => 'resize',
'contents' => in_array($label->shipment->shipmentRequest->carrier->carrierMaster->shortname, ["ups"]) ? 'true' : 'true'
]
]
]);
return $response->getBody();
} catch (\Exception $e) {
throw $e;
}
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Jobs;
use App\Models\Shipment;
use App\Models\ShipmentRequest;
use App\Services\CallbackService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Carbon\Carbon;
class ResendShipmentCallbacks implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$callbackService = App::make(CallbackService::class);
// 420282, 420900, 421010, 421017, 421332, 421490, 421591, 421649, 421629, 421625, 421630, 421626, 419164, 421615, 421634, 421099, 421638
// Query shipments created today
// $shipments = Shipment::whereDate('created_at', Carbon::today())->get();
$shipmentRequests = ShipmentRequest::whereIn('shipment_reference', [420282, 420900, 421010, 421017, 421332, 421490, 421591, 421649, 421629, 421625, 421630, 421626, 419164, 421615, 421634, 421099, 421638])
->with('shipment')
->get();
$shipments = $shipmentRequests->pluck('shipment')->filter();
foreach ($shipments as $shipment) {
if ($shipment->user->details->apiCallbackURL && $shipment->user->details->callbacks_enabled) {
$callbackService->sendCallback($shipment->user->details->apiCallbackURL, [
'endpoint' => 'shipmentProcessed',
'result' => 'true',
'shipment_reference' => $shipment->shipmentRequest->shipment_reference,
'data' => [
'trackingNumber' => $shipment->tracking_number,
'trackingURL' => $shipment->tracking_url,
],
'additionalInfo' => null
]);
}
}
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class RetryCallback implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $url;
public $data;
public $retryCount;
/**
* Create a new job instance.
*
* @param string $url
* @param array $data
* @param int $retryCount
*/
public function __construct($url, $data, $retryCount = 0)
{
$this->url = $url;
$this->data = $data;
$this->retryCount = $retryCount;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$client = new Client();
try {
$response = $client->post($this->url, [
'json' => $this->data,
'verify' => false,
]);
$responseCode = $response->getStatusCode();
$responseBody = trim(strtolower($response->getBody()->getContents()));
// Remove BOM if present
$responseBody = preg_replace('/^\xEF\xBB\xBF/', '', $responseBody);
if ($responseBody === 'true') {
Log::channel('callbacks')->info("Callback confirmed", [
'url' => $this->url,
'data' => $this->data,
'response' => $responseBody,
]);
} else {
Log::channel('callbacks')->warning("Callback not confirmed, adding to queue", [
'data' => $this->data,
"url" => $this->url,
"response" => $responseBody,
"response_hex" => bin2hex($responseBody),
]);
$this->requeueJob();
}
} catch (\Exception $e) {
Log::channel('callbacks')->error("Callback failed", [
'url' => $this->url,
'data' => $this->data,
'error' => $e->getMessage(),
]);
$this->requeueJob();
}
}
/**
* Requeue the job with an increasing delay.
*/
private function requeueJob()
{
if ($this->retryCount < 3) {
$delay = match ($this->retryCount) {
0 => now()->addHour(),
1 => now()->addHours(2),
2 => now()->addHours(4),
default => null,
};
Log::channel('callbacks')->warning("Re-queuing callback", [
'url' => $this->url,
'data' => $this->data,
'retry_count' => $this->retryCount + 1,
'next_attempt_in' => $delay,
]);
// Dispatch to the `callback_retry` queue
self::dispatch($this->url, $this->data, $this->retryCount + 1)
->delay($delay)
->onQueue('callback_retry');
} else {
Log::channel('callbacks')->error("Callback retries exhausted", [
'url' => $this->url,
'data' => $this->data,
]);
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
class SortStockWithdrawal implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Handle the job execution.
*/
public function handle()
{
try {
Log::channel('storage_management')->info("Stock withdrawal sorting...", [
'timestamp' => now()->format('d-m-y H:i:s'),
]);
$stock_first_array = Cache::get('stock_withdrawal_data', []);
usort($stock_first_array, function ($a, $b) {
return $b['to_be_withdrawn'] <=> $a['to_be_withdrawn'];
});
Cache::put('stock_withdrawal_data', $stock_first_array, now()->addDay());
Cache::put('stock_withdrawal_data_last_update', now()->format('d-m-y H:i:s'), now()->addDay());
Log::channel('storage_management')->info("Stock withdrawal sorting done!", [
'timestamp' => now()->format('d-m-y H:i:s'),
]);
}
catch (\Exception $e) {
Log::channel('storage_management')->error("FAILED - Stock withdrawal sorting failed!", [
'timestamp' => now()->format('d-m-y H:i:s'),
'error' => $e->getMessage(),
]);
throw $e;
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Jobs;
use App\Models\Shipment;
use App\Models\ShipmentTrackingStatus;
use App\Models\ShipmentTrackingStatusHistory;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class StartStockWithdrawal implements ShouldQueue
{
public function __invoke()
{
Log::channel('storage_management')->info('----------------------------------------------------');
UpdateStockWithdrawalCache::dispatch()->onQueue('storage_management');
Log::channel('storage_management')->info('----------------------------------------------------');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateStockWithdrawalCache implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Handle the job execution.
*/
public function handle()
{
Log::channel('storage_management')->info("Stock withdrawal cache update start.", [
'timestamp' => now()->format('d-m-y H:i'),
]);
// Clear the cache
Cache::forget('stock_withdrawal_data');
Cache::forget('stock_withdrawal_data_last_update');
// Retrieve items from the database
$items = DB::table('items_storage_removal as isr')
->join('shipment_request_items as sri', 'isr.shipment_request_item_id', '=', 'sri.id')
->where('isr.entry_processed', 0)
->select('sri.item_id_internal_warehouse', 'sri.quantity', 'sri.name', 'sri.model_number', 'sri.id')
->get();
// Dispatch a job for each item
ProcessStockWithdrawalItem::dispatch($items)->onQueue('storage_management');
Log::channel('storage_management')->info("Stock withdrawal job dispatched.", [
'timestamp' => now()->format('d-m-y H:i'),
]);
// SortStockWithdrawal::dispatch()->onQueue('storage_management')->delay(now()->addMinutes(10));
//
// Log::channel('storage_management')->info("Stock withdrawal sorting job dispatched to be executed in 10 minutes..", [
// 'timestamp' => now()->format('d-m-y H:i'),
// ]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ActionLog extends Model
{
use HasFactory;
protected $table = 'action_log';
protected $fillable = [
'name',
'description',
'user_id',
'shipment_request_id'
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Carrier extends Model
{
protected $table = 'carrier';
// Use the other database connection
protected $connection = 'vat_pps';
protected $fillable = [
'internal',
'ext_id',
'carrier_name',
'carrier_shortname',
'pickup_points',
'carrier_contract',
'customs_declarations',
'cod_allowed',
'cod_enabled',
'cod_price',
'country',
'zone_id',
'currency',
'img',
'enabled_store',
'shipping_price',
'free_shipping_enabled',
'delivery_time',
'display_order',
'api_allowed',
'carrier_master_id',
'carrier_base_pricing_id',
];
// If you want to disable timestamps (created_at, updated_at)
public $timestamps = false;
public function shipmentRequests()
{
return $this->hasMany(ShipmentRequest::class, 'carrier_id');
}
public function carrierMaster()
{
return $this->belongsTo(CarrierMaster::class, 'carrier_master_id');
}
public function userCarrierPricings()
{
return $this->hasMany(UserCarrierPricing::class, 'carrier_id');
}
public function basePricing()
{
return $this->hasOne(CarrierBasePricing::class, 'carrier_id');
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarrierBasePricing extends Model
{
use HasFactory;
// Define the table name if it doesn't follow Laravel's naming convention
protected $table = 'carrier_base_pricing';
protected $connection = 'mariadb';
// Specify the columns that can be mass-assigned
protected $fillable = [
'carrier_id',
'cod_price',
];
public function pricingWeights()
{
return $this->hasMany(CarrierBasePricing2Weight::class, 'carrier_base_pricing_id');
}
public function extraFees()
{
return $this->hasMany(CarrierBasePricingExtraFees::class, 'carrier_base_pricing_id');
}
public function carrier()
{
return $this->belongsTo(Carrier::class, 'carrier_id');
}
public function getPriceForWeight($weight)
{
$price = null;
$sortedWeights = $this->pricingWeights->sortByDesc('weight_max');
foreach ($sortedWeights as $pricingWeight) {
if ($weight <= $pricingWeight->weight_max) {
$price = $pricingWeight->shipping_price;
}
}
return $price;
}
public function addCodExtraFee()
{
// Fetch the 'COD' extra fee type ID
$codFeeType = CarrierExtraFeeTypes::where('name', 'COD')->first();
if (!$codFeeType) {
// Handle the case where the 'COD' type does not exist
throw new \Exception("COD fee type not found.");
}
// Create a new entry in UserCarrierPricingExtraFees
CarrierBasePricingExtraFees::create([
'carrier_base_pricing_id' => $this->id,
'carrier_extra_fee_type_id' => $codFeeType->id,
'carrier_extra_fee_value' => $this->cod_price, // Use cod_price from current UserCarrierPricing instance
'carrier_extra_fee_value_type' => 'monetary', // Set value type as 'monetary'
]);
}
public function updateCodExtraFee()
{
// Fetch the 'COD' extra fee type ID
$codFeeType = CarrierExtraFeeTypes::where('name', 'COD')->first();
if (!$codFeeType) {
// Handle the case where the 'COD' type does not exist
throw new \Exception("COD fee type not found.");
}
// Find the existing extra fee entry
$extraFee = CarrierBasePricingExtraFees::where('carrier_base_pricing_id', $this->id)
->where('carrier_extra_fee_type_id', $codFeeType->id)
->first();
if ($extraFee) {
// Update the existing entry
$extraFee->update([
'carrier_extra_fee_value' => $this->cod_price, // Update with the new cod_price
]);
} else {
// If no entry exists, create a new one
$this->addCodExtraFee();
}
}
protected static function booted()
{
parent::boot();
static::created(function ($userCarrierPricing) {
$userCarrierPricing->addCodExtraFee();
});
static::updated(function ($userCarrierPricing) {
$userCarrierPricing->updateCodExtraFee();
});
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarrierBasePricing2Weight extends Model
{
use HasFactory;
// Define the table name if it doesn't follow Laravel's naming convention
protected $table = 'carrier_base_pricing2weight';
protected $connection = 'mariadb';
// Specify the columns that can be mass-assigned
protected $fillable = [
'carrier_base_pricing_id',
'weight_max',
'shipping_price',
];
// Relationship with the CarrierBasePricing model
public function carrierBasePricing()
{
return $this->belongsTo(CarrierBasePricing::class, 'carrier_base_pricing_id');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarrierBasePricingExtraFees extends Model
{
use HasFactory;
// Define the table name if it doesn't follow Laravel's naming convention
protected $table = 'carrier_base_pricing_extra_fees';
protected $connection = 'mariadb';
// Specify the columns that can be mass-assigned
protected $fillable = [
'carrier_base_pricing_id',
'carrier_extra_fee_type_id',
'carrier_extra_fee_value',
'carrier_extra_fee_value_type',
];
// Relationship with the CarrierBasePricing model
public function carrierBasePricing()
{
return $this->belongsTo(CarrierBasePricing::class, 'carrier_base_pricing_id');
}
public function extraFeeType()
{
return $this->belongsTo(CarrierExtraFeeTypes::class, 'carrier_extra_fee_type_id');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarrierExtraFeeTypes extends Model
{
use HasFactory;
protected $table = 'carrier_extra_fee_types';
protected $fillable = [
'name',
'description',
];
public function extraFees()
{
return $this->hasMany(UserCarrierPricingExtraFees::class, 'carrier_extra_fee_type_id');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CarrierMaster extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'carrier_master';
protected $connection = 'mariadb';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'shortname',
'display_name',
'img',
'api_url',
'api_url_sandbox',
'label_reprint',
'carrier_enabled',
'last_pickup'
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'label_reprint' => 'boolean',
'carrier_enabled' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function carriers()
{
return $this->hasMany(Carrier::class, 'carrier_master_id');
}
}

View File

@ -0,0 +1,20 @@
<?php
// app/Models/CarrierPricingGroup.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class CarrierPricingGroup extends Model
{
use HasFactory;
protected $fillable = ['name', 'base_ratio'];
public function userCarrierPricingGroups(): HasMany
{
return $this->hasMany(UserCarrierPricingGroup::class);
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarrierUserCredential extends Model
{
use HasFactory;
protected $table = 'carrier_user_credentials';
protected $fillable = [
'user_id',
'name',
'type',
'sandbox',
'carrier_master_id', // Updated field name
'value',
'expiry_date'
];
// Encrypt the value before saving to the database
protected function setValueAttribute($value)
{
$this->attributes['value'] = encrypt($value);
}
// Decrypt the value when retrieving from the database
protected function getValueAttribute($value)
{
return decrypt($value);
}
// Define the relationship with CarrierMaster
public function carrierMaster()
{
return $this->belongsTo(CarrierMaster::class, 'carrier_master_id');
}
/**
* Get credentials by carrier shortname and sandbox status.
*
* @param string $carrierShortname
* @param bool $sandbox
* @return \Illuminate\Support\Collection
*/
public static function getCredentialsByCarrier($carrierShortname, $sandbox = false)
{
// Get the carrier_master_id using the shortname
$carrierMaster = CarrierMaster::where('shortname', $carrierShortname)->first();
if (!$carrierMaster) {
return collect([]);
}
$credentials = self::where('carrier_master_id', $carrierMaster->id)
->where('sandbox', $sandbox)
->get();
$creds = [];
foreach ($credentials as $credential) {
$creds[$credential->type] = $credential;
}
return collect($creds);
}
}

View File

@ -0,0 +1,44 @@
<?php
// app/Models/GeneratedFile.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class GeneratedFile extends Model
{
use HasFactory;
protected $table = 'generated_files';
protected $fillable = [
'filename',
'user_id',
'file_data',
'lifetime',
'associated_id',
'type',
'created_at',
'updated_at'
];
protected $casts = [
'lifetime' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
?>

View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ItemsStorageRemoval extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'items_storage_removal';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'shipment_request_item_id',
'entry_processed',
];
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
/**
* Define the relationship with ShipmentRequestItem.
*/
public function shipmentRequestItem()
{
return $this->belongsTo(ShipmentRequestItem::class, 'shipment_request_item_id');
}
}

79
src/app/Models/Label.php Normal file
View File

@ -0,0 +1,79 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class Label extends Model
{
use HasFactory;
protected $fillable = [
'id',
'shipment_id',
'label_data',
'temporary'
];
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* Get the shipment that owns the label.
*/
public function shipment()
{
return $this->belongsTo(Shipment::class, 'shipment_id');
}
/**
* Custom create method to modify data before storing.
*
* @param array $data
* @return static
*/
public static function storeEntry(string $label, Shipment $shipment): static
{
return self::create(array(
"label_data" => $label,
"shipment_id" => $shipment->id
));
}
public static function createInstance(string $label, Shipment $shipment): static
{
return self::create(array(
"label_data" => $label,
"shipment_id" => $shipment->id,
"temporary" => true
));
}
protected static function booted()
{
parent::boot();
static::saving(function ($labelDetails) {
ActionLog::create([
'name' => "label_created",
'description' => "Label created for shipment, reference no: '{$labelDetails->shipment->shipmentRequest->shipment_reference}'",
'user_id' => Auth::user()->id ?? 1,
'shipment_request_id' => $labelDetails->shipment->shipmentRequest->id
]);
Log::channel('expedice')->info('booted saving initiated in Label model - label created by ' . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $labelDetails->shipment->shipmentRequest->id,
'user_id' => Auth::user()->id ?? 1,
]);
});
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class PickupPoint extends Model
{
protected $table = 'pickup_point';
protected $connection = 'vat_pps';
// Fillable attributes for mass assignment
protected $fillable = [
'ext_id',
'carrier_id',
'name',
'type',
'directions',
'place_name',
'address',
'zip',
'city',
'country_iso',
'status_id',
'card',
'cod',
'wheelchair',
'photo_url',
'thumbnail_url',
'enabled'
];
// The attributes that should be mutated to dates
protected $dates = ['updated_at'];
protected $hidden = ['location'];
// Define relationship with Carrier
public function carrier()
{
return $this->belongsTo(Carrier::class, 'carrier_id');
}
public static function getNearbyPickupPoints($extId, $carrier_id, $limit = 20)
{
// Get the pickup point by ext_id
$pickupPoint = PickupPoint::where('ext_id', $extId)
->where('carrier_id', $carrier_id)
->first();
// If the pickup point exists, find nearby pickup points
if ($pickupPoint) {
// Use Eloquent to build the query with distance calculation
return PickupPoint::selectRaw("*, ST_Distance_Sphere(location, ?) as distance", [$pickupPoint->location])
->where('ext_id', '!=', $extId) // Exclude the current pickup point
->orderBy('distance') // Order by distance
->limit($limit) // Limit the number of results
->get(); // Return a collection of PickupPoint models
}
// Return an empty collection if no pickup point was found
return collect();
}
}

109
src/app/Models/Shipment.php Normal file
View File

@ -0,0 +1,109 @@
<?php
namespace App\Models;
use App\Services\CallbackService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class Shipment extends Model
{
protected $table = 'shipment';
use HasFactory;
protected $fillable = [
'user_id',
'shipment_request_id',
'internal_shipment_id',
'shipment_id',
'tracking_number',
'tracking_url',
];
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected static function booted()
{
parent::boot();
static::saving(function ($shipmentDetails) {
$callbackService = App::make(CallbackService::class);
ActionLog::create([
'name' => "shipment_created",
'description' => "Shipment created, no: '{$shipmentDetails->tracking_number}'",
'user_id' => Auth::user()->id ?? 1,
'shipment_request_id' => $shipmentDetails->shipmentRequest->id
]);
Log::channel('expedice')->info('booted saving initiated in Shipment model - shipment created by ' . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentDetails->shipmentRequest->id,
'user_id' => Auth::user()->id ?? 1,
]);
if ($shipmentDetails->user->details->apiCallbackURL && $shipmentDetails->user->details->callbacks_enabled) {
$callbackService->sendCallback($shipmentDetails->user->details->apiCallbackURL, [
'endpoint' => 'shipmentProcessed',
'result' => 'true',
'shipment_reference' => $shipmentDetails->shipmentRequest->shipment_reference,
'data' => [
'trackingNumber' => $shipmentDetails->tracking_number,
'trackingURL' => $shipmentDetails->tracking_url,
],
'additionalInfo' => null
]);
}
foreach ($shipmentDetails->shipmentRequest->errors as $error) {
$error->update([
'resolved' => true,
]);
}
});
}
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
/**
* Get the user that owns the shipment.
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function label()
{
return $this->hasOne(Label::class, 'shipment_id');
}
public function processedBaseFees()
{
return $this->hasMany(ShipmentProcessedBaseFee::class, 'shipment_id');
}
public function processedExtraFees()
{
return $this->hasMany(ShipmentProcessedExtraFee::class, 'shipment_id');
}
public function trackingStatusHistory()
{
return $this->hasMany(ShipmentTrackingStatusHistory::class, 'shipment_id');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentErrorLog extends Model
{
use HasFactory;
protected $table = 'shipment_error_log';
protected $fillable = [
'shipment_request_id',
'error_message',
'fetched',
'resolved',
];
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
public function batchItems()
{
return $this->hasManyThrough(
ShipmentRequestBatchItem::class,
ShipmentRequest::class,
'id', // Foreign key on ShipmentRequest table
'shipment_request_id', // Foreign key on ShipmentRequestBatchItem table
'shipment_request_id', // Local key on ShipmentErrorLog table
'id' // Local key on ShipmentRequest table
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentProcessedBaseFee extends Model
{
use HasFactory;
// Define the table name explicitly
protected $table = 'shipment_processed_base_fees';
// Specify the columns that can be mass-assigned
protected $fillable = [
'shipment_id',
'base_fee_type',
'base_fee_value',
'base_fee_desc',
];
// Define the casts for date attributes
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function shipment()
{
return $this->belongsTo(Shipment::class, 'shipment_id');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentProcessedExtraFee extends Model
{
use HasFactory;
// Define the table name explicitly
protected $table = 'shipment_processed_extra_fees';
// Specify the columns that can be mass-assigned
protected $fillable = [
'shipment_id',
'extra_fee_type_id',
'extra_fee_type_value',
'extra_fee_desc',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function shipment()
{
return $this->belongsTo(Shipment::class, 'shipment_id');
}
}

View File

@ -0,0 +1,226 @@
<?php
namespace App\Models;
use App\Services\CallbackService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Models\ItemsStorageRemoval;
class ShipmentRequest extends Model
{
protected $table = 'shipment_request';
protected $fillable = [
'shipment_reference',
'user_id',
'note',
'delivery_address_name',
'delivery_address_company_name',
'delivery_address_street_name',
'delivery_address_street_number',
'delivery_address_address_line_2',
'delivery_address_city',
'delivery_address_zip',
'delivery_address_state_iso',
'delivery_address_country_iso',
'pickup_point_code',
'pickup_point_name',
'pickup_point_address_street_name',
'pickup_point_address_street_number',
'pickup_point_address_city',
'pickup_point_address_zip',
'pickup_point_address_state_iso',
'pickup_point_address_country_iso',
'postnummer',
'contact_email',
'contact_telephone',
'cod_value',
'cod_variable_symbol',
'shipment_value',
'currency',
'weight',
'height',
'width',
'length',
'carrier_id',
'allegro_carrier_id',
'package_type',
'send_again_flag',
'allegro_smart',
'ups_invoice_document',
];
protected $casts = [
'cod_value' => 'decimal:2',
'shipment_value' => 'decimal:2',
'weight' => 'decimal:2',
];
public function items()
{
return $this->hasMany(ShipmentRequestItem::class, 'shipment_request_id');
}
public function shipment()
{
return $this->hasOne(Shipment::class, 'shipment_request_id');
}
protected $appends = ['delivery_address'];
/**
* Accessor to get the delivery address as an array.
*
* @return array
*/
public function getDeliveryAddressAttribute()
{
return [
'name' => $this->delivery_address_name,
'companyName' => $this->delivery_address_company_name,
'streetName' => $this->delivery_address_street_name,
'streetNumber' => $this->delivery_address_street_number,
'addressLine2' => $this->delivery_address_address_line_2,
'city' => $this->delivery_address_city,
'zip' => $this->delivery_address_zip,
'stateISO' => $this->delivery_address_state_iso,
'countryISO' => $this->delivery_address_country_iso,
];
}
public function carrier()
{
return $this->belongsTo(Carrier::class, 'carrier_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function invoice()
{
return $this->hasOne(ShipmentRequestInvoice::class, 'shipment_request_id');
}
public function batchItem()
{
return $this->hasOne(ShipmentRequestBatchItem::class, 'shipment_request_id');
}
public function batch()
{
return $this->hasOneThrough(
ShipmentRequestBatch::class,
ShipmentRequestBatchItem::class,
'shipment_request_id', // Foreign key on ShipmentRequestBatchItem table
'id', // Foreign key on ShipmentRequestBatch table
'id', // Local key on ShipmentRequest table
'shipment_request_batch_id' // Local key on ShipmentRequestBatchItem table
);
}
public function errors()
{
return $this->hasMany(ShipmentErrorLog::class, 'shipment_request_id');
}
public function unresolvedErrors()
{
return $this->hasMany(ShipmentErrorLog::class)->where('resolved', false);
}
public function shipmentStatuses()
{
return $this->hasMany(ShipmentStatusHistory::class, 'shipment_request_id');
}
public function currentShipmentStatus()
{
return $this->hasOne(ShipmentStatusHistory::class, 'shipment_request_id')
->latestOfMany(); // This ensures only the latest record is returned.
}
protected static function booted()
{
parent::boot();
// Handle saving events for both updates and first saves
static::saving(function ($shipmentRequest) {
$callbackService = App::make(CallbackService::class);
// Check if the user has an API callback URL and callbacks are enabled
if ($shipmentRequest->user->details->apiCallbackURL && $shipmentRequest->user->details->callbacks_enabled) {
// Check if the model is being created (i.e., a first-time save)
if ($shipmentRequest->isDirty() && !$shipmentRequest->exists) {
ActionLog::create([
'name' => "shipment_request_created",
'description' => "Shipment accepted to APP, reference no: '{$shipmentRequest->shipment_reference}'",
'user_id' => $shipmentRequest->user->id,
'shipment_request_id' => $shipmentRequest->id
]);
$callbackService->sendCallback($shipmentRequest->user->details->apiCallbackURL, [
'endpoint' => 'shipmentRequestCreated',
'result' => 'true',
'shipment_reference' => $shipmentRequest->shipment_reference,
'data' => [
'createdFields' => $shipmentRequest->getAttributes(), // Fields that were initially set
],
'additionalInfo' => null
]);
}
// Check if the model is being updated
else {
ActionLog::create([
'name' => "shipment_request_updated",
'description' => "Shipment updated, reference no: '{$shipmentRequest->shipment_reference}', changes: '" . var_export($shipmentRequest->getDirty(), true),
'user_id' => Auth::user()->id ?? 1,
'shipment_request_id' => $shipmentRequest->id
]);
Log::channel('expedice')->info('Shipment updated by ' . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentRequest->id,
'changes' => $shipmentRequest->getDirty(),
]);
$callbackService->sendCallback($shipmentRequest->user->details->apiCallbackURL, [
'endpoint' => 'shipmentRequestUpdated',
'result' => 'true',
'shipment_reference' => $shipmentRequest->shipment_reference,
'data' => [
'updatedFields' => $shipmentRequest->getDirty(), // Fields that were changed
],
'additionalInfo' => null
]);
}
}
});
}
public function createStorageRemovalEntries()
{
foreach ($this->items as $item) {
// Check if an entry already exists
$exists = ItemsStorageRemoval::where('shipment_request_item_id', $item->id)->exists();
// If it doesn't exist, create a new entry
if (!$exists) {
ItemsStorageRemoval::create([
'shipment_request_item_id' => $item->id,
'entry_processed' => false, // Default value
]);
}
}
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentRequestBatch extends Model
{
use HasFactory;
protected $table = 'shipment_request_batch';
protected $fillable = ['user_id', 'carrier_master_id', 'processed', 'printed'];
protected $appends = ['item_count', 'error_count'];
public function items()
{
return $this->hasMany(ShipmentRequestBatchItem::class);
}
public function generatedFiles()
{
return $this->hasMany(GeneratedFile::class, 'associated_id')
->where('type', 'batch');
}
public function latestGeneratedFile()
{
return $this->hasMany(GeneratedFile::class, 'associated_id')
->where('type', 'batch')->latest();
}
public function carrierMaster()
{
return $this->belongsTo(CarrierMaster::class, 'carrier_master_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function getItemCountAttribute()
{
return $this->items()->count();
}
public function getErrorCountAttribute()
{
return $this->items()
->whereHas('shipmentRequest.errors', function ($query) {
$query->where('resolved', false);
})
->count();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentRequestBatchItem extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'shipment_request_batch_items';
protected $fillable = ['shipment_request_batch_id', 'shipment_request_id'];
public function batch()
{
return $this->belongsTo(ShipmentRequestBatch::class, 'shipment_request_batch_id');
}
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentRequestInvoice extends Model
{
use HasFactory;
// Define the table associated with the model
protected $table = 'shipment_request_invoice';
// Define the columns that are mass assignable
protected $fillable = [
'shipment_request_id',
'invoice_number',
'invoice_date',
'invoice_currency',
'invoice_address_name',
'invoice_address_company_name',
'invoice_address_company_vat_number',
'invoice_address_street_name',
'invoice_address_street_number',
'invoice_address_city',
'invoice_address_zip',
'invoice_address_state_iso',
'invoice_address_country_iso',
'invoice_raw_data',
];
// Define the date format for created_at and updated_at columns
protected $dates = [
'created_at',
'updated_at'
];
// Define any relationships if necessary
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentRequestItem extends Model
{
protected $table = 'shipment_request_items';
use HasFactory;
protected $fillable = [
'shipment_request_id',
'name',
'item_note',
'model_number',
'HSCode',
'price',
'quantity',
'originCountry',
'weight',
'item_id_internal_warehouse',
'item_id_external',
];
public function shipment()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ShipmentStatus extends Model
{
protected $table = 'shipment_status';
use HasFactory;
protected $fillable = [
'name',
'description',
'internal',
];
public $timestamps = false;
// Define status constants
const STATUS_CREATED = 1;
const STATUS_PENDING = 2;
const STATUS_LABEL_CREATED = 3;
const STATUS_PACKED = 4;
const STATUS_IN_TRANSIT = 5;
const STATUS_DELIVERED = 6;
const STATUS_ADDRESS_INSUFFICIENT = 7;
const STATUS_OUT_OF_STOCK = 8;
const STATUS_PICKUP_UNAVAILABLE = 9;
const STATUS_EXT_PARCEL_ID_GENERATED = 10;
const STATUS_IS_REPAIR = 11;
const STATUS_PROGRAMMING_MODEL_REQUIRED = 12;
const STATUS_PROGRAMMING_PROCESS = 13;
const STATUS_PICKUP_REROUTE = 14;
const STATUS_CANCELED = 15;
const STATUS_PRINTING = 16;
const STATUS_PRINTING_UNDONE = 17;
const STATUS_EXPEDICE_DELAY = 18;
const STATUS_EXPEDICE_SPECIAL_HANDLING = 19;
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Models;
use App\Factories\CarrierServiceFactory;
use App\Services\CallbackService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ShipmentStatusHistory extends Model
{
protected $table = 'shipment_status_history';
use HasFactory;
protected $fillable = [
'shipment_request_id',
'shipment_status_id',
'status_note',
];
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* Get the shipment that owns the status history.
*/
public function shipmentRequest()
{
return $this->belongsTo(ShipmentRequest::class, 'shipment_request_id');
}
/**
* Get the shipment status that owns the status history.
*/
public function shipmentStatus()
{
return $this->belongsTo(ShipmentStatus::class, 'shipment_status_id');
}
protected static function booted()
{
parent::boot();
static::creating(function ($shipmentStatusHistory) {
$callbackService = App::make(CallbackService::class);
ActionLog::create([
'name' => "status_changed",
'description' => "Status changed to '{$shipmentStatusHistory->shipmentStatus->name}'",
'user_id' => Auth::user()->id ?? 1,
'shipment_request_id' => $shipmentStatusHistory->shipmentRequest->id
]);
Log::channel('expedice')->info("Status changed to {$shipmentStatusHistory->shipmentStatus->name} by " . (Auth::user()->name ?? '///'), [
'shipment_request_id' => $shipmentStatusHistory->shipmentRequest->id
]);
if(in_array($shipmentStatusHistory->shipment_status_id, [
ShipmentStatus::STATUS_ADDRESS_INSUFFICIENT,
ShipmentStatus::STATUS_OUT_OF_STOCK,
ShipmentStatus::STATUS_IS_REPAIR,
ShipmentStatus::STATUS_PROGRAMMING_MODEL_REQUIRED,
ShipmentStatus::STATUS_PROGRAMMING_PROCESS,
ShipmentStatus::STATUS_CANCELED,
])) {
// auto batch remove statuses
if (!is_null($shipmentStatusHistory->shipmentRequest->batch)) {
ShipmentRequestBatchItem::where('shipment_request_id', $shipmentStatusHistory->shipmentRequest->id)->delete();
}
}
if($shipmentStatusHistory->shipment_status_id === ShipmentStatus::STATUS_CANCELED) {
try {
if(!empty($shipmentStatusHistory->shipmentRequest->shipment)) {
$carrierService = CarrierServiceFactory::create($shipmentStatusHistory->shipmentRequest->carrier);
$res = $carrierService->cancelShipment($shipmentStatusHistory->shipmentRequest->shipment);
if ($res) {
$shipmentStatusHistory->shipmentRequest?->shipment?->label?->delete();
$shipmentStatusHistory->shipmentRequest?->shipment?->delete();
}
}
}
catch(\Exception $e) {
}
}
// Example of a callback or logging action when a new status history is created
if ($shipmentStatusHistory->shipmentRequest->user->details->apiCallbackURL && $shipmentStatusHistory->shipmentRequest->user->details->callbacks_enabled) {
$callbackService->sendCallback($shipmentStatusHistory->shipmentRequest->user->details->apiCallbackURL, [
'endpoint' => 'shipmentStatusHistoryCreated',
'result' => 'true',
'data' => [
'trackingNumber' => $shipmentStatusHistory->shipmentRequest->shipment?->tracking_number ?? "",
'trackingURL' => $shipmentStatusHistory->shipmentRequest->shipment?->tracking_url ?? "",
],
'shipment_reference' => $shipmentStatusHistory->shipmentRequest->shipment_reference,
'status_id' => $shipmentStatusHistory->shipment_status_id,
'status_name' => $shipmentStatusHistory->shipmentStatus->name,
'status_desc' => $shipmentStatusHistory->shipmentStatus->description,
]);
}
});
}
}

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