Compare commits

..

No commits in common. "f58c709945f5454a5445d31d83ab9dad8fa380e7" and "0341c9c189bbb21ebbaf56bd787c2a8830658c71" have entirely different histories.

2350 changed files with 60025 additions and 289471 deletions

View file

@ -1,2 +0,0 @@
WWWUSER=501
WWWGROUP=20

View file

@ -1,2 +0,0 @@
WWWUSER=501
WWWGROUP=20

View file

@ -1,231 +0,0 @@
# Laravel Sail DevContainer Setup
Diese Dokumentation beschreibt die Einrichtung und Verwendung des DevContainers für das Laravel Sail-Projekt.
## Übersicht
Der DevContainer ermöglicht es, das Laravel-Projekt in einer vollständig konfigurierten Docker-Umgebung zu entwickeln, ohne dass lokale PHP-, Node.js- oder andere Abhängigkeiten installiert werden müssen.
## Voraussetzungen
- Docker Desktop installiert und laufend
- Cursor.ai oder VS Code mit DevContainer-Erweiterung
- Git (für Repository-Zugriff)
## Konfiguration
### 1. DevContainer-Konfiguration (`devcontainer.json`)
Die Hauptkonfiguration befindet sich in `.devcontainer/devcontainer.json`:
- **Eigenständiges Docker-Image**: Verwendet `sail-8.4/app` direkt ohne Docker Compose
- **Build-Konfiguration**: Definiert Build-Argumente für `WWWUSER` und `WWWGROUP`
- **Workspace**: `/var/www/html` (Standard-Laravel-Verzeichnis)
- **Benutzer**: `sail` (Laravel Sail Standard-Benutzer)
- **Features**: Leer (Tools werden über postCreateCommand installiert)
- **Extensions**: PHP, Laravel Blade, Tailwind CSS Extensions
- **Port-Forwarding**: Automatisch für wichtige Services (5173, 33061, 6380, 8025)
### 2. Dockerfile-Anpassungen (`docker/8.4/Dockerfile`)
Das Dockerfile wurde angepasst um:
- `ARG WWWUSER` hinzugefügt für korrekte Benutzer-ID
- Benutzererstellung mit dynamischen IDs (`$WWWUSER` statt feste `1337`)
- Vollständige Laravel-Umgebung mit PHP 8.4, Composer, NPM, Git
## Installation
### Automatische Installation (Empfohlen)
1. Öffnen Sie das Projekt in Cursor.ai oder VS Code
2. Klicken Sie auf "Reopen in Container" wenn die DevContainer-Benachrichtigung erscheint
3. Warten Sie bis der Container gebaut und gestartet ist
### Manuelle Installation
Falls die automatische Installation fehlschlägt, können Sie den Container manuell bauen:
```bash
cd /Users/pandora/Sites/mivita.care
docker build --build-arg WWWUSER=501 --build-arg WWWGROUP=20 -f docker/8.4/Dockerfile -t sail-8.4/app docker/8.4
```
## Verfügbare Tools
Nach der Installation sind folgende Tools verfügbar:
- **PHP 8.4.12**: Mit Xdebug für Debugging
- **Composer**: Für Laravel-Abhängigkeiten
- **NPM**: Für Frontend-Assets
- **Git**: Für Versionskontrolle
- **Node.js**: Für JavaScript-Entwicklung
Überprüfen Sie die Installation:
```bash
which git
which npm
which composer
php --version
```
## Post-Create-Befehle
Nach dem Erstellen des Containers werden automatisch ausgeführt:
1. Composer-Abhängigkeiten (`composer install`)
## Entwicklung
### Erste Schritte
1. **Composer-Abhängigkeiten**: Werden automatisch installiert
2. **Laravel-Konfiguration**: `.env` Datei muss manuell erstellt werden (siehe Laravel-Dokumentation)
3. **Datenbank-Migrationen**: `php artisan migrate`
4. **Asset-Kompilierung**: `npm install && npm run dev`
### Häufige Befehle
```bash
# Laravel-Befehle
php artisan migrate
php artisan serve
php artisan tinker
# Composer
composer install
composer update
# NPM/Node
npm install
npm run dev
npm run build
# Sail-Befehle (falls verfügbar)
./vendor/bin/sail up
./vendor/bin/sail artisan migrate
```
## Troubleshooting
### Container startet nicht
1. Überprüfen Sie ob Docker Desktop läuft
2. Stellen Sie sicher, dass keine anderen Container die benötigten Ports blockieren
3. Versuchen Sie einen Clean-Build: `docker build --no-cache ...`
### Berechtigungsprobleme
Die Container sind so konfiguriert, dass sie mit Ihrer lokalen Benutzer-ID (`501`) laufen, um Berechtigungsprobleme zu vermeiden.
### NPM Global Install Probleme
Falls Sie globale NPM-Pakete installieren möchten, verwenden Sie lokale Installation:
```bash
# Lokale Installation (empfohlen)
npm install package-name
# Oder NPM-Prefix ändern
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
### Claude CLI Kompatibilitätsprobleme
**Problem**: Das `@anthropic-ai/claude-code` Paket ist nicht mit modernen Node.js-Versionen kompatibel.
**Symptome**:
- Viele veraltete Abhängigkeiten (deprecated packages)
- PhantomJS ARM64 Kompatibilitätsprobleme
- Engine-Konflikte mit Node.js v22
**Lösung**:
- Verwenden Sie die Cursor.ai-Integration direkt (empfohlen)
- Oder verwenden Sie alternative CLI-Tools
- Das Paket ist für die Laravel-Entwicklung nicht notwendig
### Port-Konflikte
Falls Ports bereits belegt sind, können Sie die Port-Mapping in der DevContainer-Konfiguration anpassen.
## Erweiterte Konfiguration
### Zusätzliche Extensions hinzufügen
Bearbeiten Sie `devcontainer.json` und fügen Sie Extensions zum `extensions` Array hinzu:
```json
"extensions": [
"bmewburn.vscode-intelephense-client",
"onecentlin.laravel-blade",
"shufo.vscode-blade-formatter",
"bradlc.vscode-tailwindcss",
"ihrc.vscode-php-cs-fixer"
]
```
### Zusätzliche Tools installieren
Fügen Sie Tools zum `postCreateCommand` hinzu:
```json
"postCreateCommand": "composer install --no-interaction --prefer-dist --optimize-autoloader && npm install -g your-tool"
```
## Support
Bei Problemen mit dem DevContainer:
1. Überprüfen Sie die Docker-Logs: `docker logs <container-name>`
2. Stellen Sie sicher, dass alle Umgebungsvariablen korrekt gesetzt sind
3. Versuchen Sie einen Neustart des DevContainers
## Bekannte Probleme und Lösungen
### DevContainer-CLI Kompatibilität
**Problem**: DevContainer-CLI erstellt temporäre Docker-Compose-Dateien die mit der Konfiguration kollidieren.
**Lösung**: Verwendung eines eigenständigen Docker-Images ohne Docker Compose-Abhängigkeit.
### Features-Installation
**Problem**: DevContainer Features können zu Build-Fehlern führen.
**Lösung**: Tools werden über `postCreateCommand` installiert statt über Features.
### Root-Berechtigungen
**Problem**: Der `sail`-Benutzer hat keine Root-Rechte für Paket-Installation.
**Lösung**: Alle notwendigen Tools sind bereits im Docker-Image installiert.
### Claude CLI Kompatibilität
**Problem**: Das `@anthropic-ai/claude-code` Paket ist veraltet und nicht mit Node.js v22 kompatibel.
**Symptome**:
- Hunderte von deprecated package warnings
- PhantomJS ARM64 Kompatibilitätsprobleme
- Engine-Konflikte mit modernen Node.js-Versionen
**Lösung**:
- **Verwenden Sie Cursor.ai direkt** (empfohlen) - keine CLI nötig
- Das Paket ist für Laravel-Entwicklung nicht notwendig
- Alle wichtigen Tools (PHP, Composer, NPM, Git) sind bereits verfügbar
---
**Letzte Aktualisierung**: September 2025
**Laravel Version**: 11.x
**PHP Version**: 8.4.12
**Docker Version**: 2.x
**Status**: ✅ Vollständig funktionsfähig

View file

@ -1,45 +0,0 @@
{
"name": "Mivita Care (Dev Container)",
"dockerComposeFile": [
"../docker-compose.yml"
],
"service": "laravel.test",
"workspaceFolder": "/var/www/html",
"remoteUser": "sail",
"features": {},
"customizations": {
"vscode": {
"extensions": [
"bmewburn.vscode-intelephense-client",
"onecentlin.laravel-blade",
"shufo.vscode-blade-formatter",
"bradlc.vscode-tailwindcss",
"Anthropic.claude-code",
"onecentlin.laravel-extension-pack"
]
}
},
// WICHTIG: Hier stehen jetzt nur noch die Dienste, die es im Projekt wirklich gibt!
"runServices": [
"laravel.test",
"horizon"
],
"containerEnv": {
"WWWUSER": "501",
"WWWGROUP": "20",
"LARAVEL_SAIL": "1"
},
"mounts": [
"source=${localWorkspaceFolder},target=/var/www/html,type=bind,consistency=cached"
],
// WICHTIG: Nur noch der Vite-Port muss weitergeleitet werden, den Rest macht das Mutterschiff.
"forwardPorts": [
5173
],
"portsAttributes": {
"5173": {
"label": "Vite Dev Server",
"onAutoForward": "notify"
}
}
}

View file

@ -1,125 +0,0 @@
services:
laravel.test:
build:
context: '../docker/8.4'
dockerfile: Dockerfile
args:
# Führen Sie in Ihrem normalen Terminal `id -u` aus und tragen Sie die Zahl hier ein.
WWWUSER: '501'
# Führen Sie in Ihrem normalen Terminal `id -g` aus und tragen Sie die Zahl hier ein.
WWWGROUP: '20'
image: 'sail-8.4/app'
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '5173:5173'
environment:
# Laravel Sail Environment Variables
WWWUSER: '501'
WWWGROUP: '20'
LARAVEL_SAIL: 1
XDEBUG_MODE: 'develop,debug'
XDEBUG_CONFIG: 'client_host=host.docker.internal'
IGNITION_LOCAL_SITES_PATH: '/var/www/html'
# Database Configuration
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: mivita
DB_USERNAME: sail
DB_PASSWORD: password
# Application Configuration
APP_NAME: Mivita
APP_ENV: local
APP_DEBUG: true
APP_URL: http://localhost
# Mail Configuration
MAIL_MAILER: smtp
MAIL_HOST: mailpit
MAIL_PORT: 1025
MAIL_USERNAME: null
MAIL_PASSWORD: null
MAIL_ENCRYPTION: null
MAIL_FROM_ADDRESS: hello@example.com
MAIL_FROM_NAME: Mivita
# Redis Configuration
REDIS_HOST: redis
REDIS_PASSWORD: null
REDIS_PORT: 6379
# Vite Configuration
VITE_PORT: 5173
# Forward Ports
FORWARD_DB_PORT: 33061
FORWARD_REDIS_PORT: 6380
FORWARD_MAILPIT_PORT: 1025
FORWARD_MAILPIT_DASHBOARD_PORT: 8025
# MySQL Extra Options
MYSQL_EXTRA_OPTIONS: --default-authentication-plugin=mysql_native_password
volumes:
- '../:/var/www/html'
networks:
- sail
depends_on:
- mysql
- redis
- mailpit
mysql:
image: 'mysql/mysql-server:8.0'
ports:
- '33061:3306'
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: mivita
MYSQL_USER: sail
MYSQL_PASSWORD: password
MYSQL_ALLOW_EMPTY_PASSWORD: 1
MYSQL_EXTRA_OPTIONS: --default-authentication-plugin=mysql_native_password
volumes:
- 'sail-mysql:/var/lib/mysql'
- '../docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- mysqladmin
- ping
- '-ppassword'
retries: 3
timeout: 5s
redis:
image: 'redis:alpine'
ports:
- '6380:6379'
volumes:
- 'sail-redis:/data'
networks:
- sail
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
mailpit:
image: 'axllent/mailpit:latest'
ports:
- '1025:1025'
- '8025:8025'
networks:
- sail
networks:
sail:
driver: bridge
volumes:
sail-mysql:
driver: local
sail-redis:
driver: local

140
.env
View file

@ -3,7 +3,6 @@ APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:HrWQ9AV3Zt2TU0iq1OeUUpTUaXwNUdh8xHmx7RXTif4=
APP_URL=https://mivita.test/
APP_URL_CRM=https://my.mivita.test
APP_DOMAIN=mivita
APP_TLD_CARE=.test
APP_TLD_SHOP=.lshop
@ -11,78 +10,52 @@ APP_PROTOCOL=https://
APP_URL_MAIN=
APP_URL_CHECKOUT=checkout.
#APP_URL_MAIN=dev.
APP_PRE_URL_CRM=my.
APP_URL_PORTAL=in.
APP_PRE_URL_PORTAL=in.
APP_URL_CRM=my.
#APP_CHECKOUT_MAIL=no-reply@mivita.care
APP_CONTACT_MAIL=kevin.adametz@me.com
APP_CHECKOUT_MAIL=kevin.adametz@me.com
APP_INFO_MAIL=kevin.adametz@me.com
APP_PROORITY_MAIL=kevin.adametz@me.com
APP_DEFAULT_MAIL=kevin.adametz@me.com
APP_CHECKOUT_TEST_MAIL=kevin.adametz@me.com
APP_INFO_TEST_MAIL=kevin.adametz@me.com
EXCEPTION_MAIL=exception@adametz.media
SESSION_DOMAIN=.mivita.test
#SESSION_DOMAIN=.mivita.care
BUSINESS_FORCE_EXECUTE=true
APP_MODE=test
APP_IPINFO=true
APP_MAIN_TAX=1.19
APP_MAIN_TAX_RATE=19
APP_SHIPPING_TAX=19
APP_PHP_VERSION=8.2
APP_MAIN_TAX = 1.19
APP_MAIN_TAX_RATE = 19
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=global-mysql
DB_HOST=192.168.1.8
DB_PORT=3306
DB_DATABASE=mivita
DB_USERNAME=root
DB_PASSWORD=password
MYSQL_EXTRA_OPTIONS=
#DB_HOST=192.168.1.8
#DB_PORT=3306
#DB_DATABASE=mivita
#DB_USERNAME=kadmin
#DB_PASSWORD=password
DB_USERNAME=kadmin
DB_PASSWORD=KT32vQ7ix
#DB_DATABASE=d02c1ed2
#DB_USERNAME=d02c1ed2
#DB_PASSWORD=password
#DB_PASSWORD=H7mdYuVTV6pNHDVu
PAYONE_URL=checkout.mivita.care
PAYONE_TS=checkout.mivita.care/transaction/status
PAYONE_KEY=Zjop5cvP2UeB7Qhy
PAYONE_URL = checkout.mivita.care
PAYONE_TS = checkout.mivita.care/transaction/status
PAYONE_KEY = Zjop5cvP2UeB7Qhy
BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
QUEUE_DRIVER=redis
QUEUE_DRIVER=sync
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
#MAIL_USERNAME=m04a9fbc
#MAIL_PASSWORD=3tQ72oCHZgncCTpK
#MAIL_ENCRYPTION=null
MAIL_HOST=w017f6e4.kasserver.com
MAIL_PORT=587
MAIL_USERNAME=m04a9fbc
MAIL_PASSWORD=3tQ72oCHZgncCTpK
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
@ -93,87 +66,10 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIVITA_RENEWAL_DAYS=29
MIVITA_REMIND_FIRST_DAYS=21
MIVITA_REMIND_SEC_DAYS=14
MIVITA_ABO_BOOKING_DAYS=7
MIVITA_REMIND_LAST_DAYS=2
MIVITA_EDIT_DATA_PASS=mivita
MIVITA_ADD_NUMBER_ID=946
# =============================================================================
# =============================================================================
# DHL VERSANDMODUL KONFIGURATION (acme/laravel-dhl)
# =============================================================================
# =============================================================================
# DHL KONFIGURATION - PRIORITÄT
# =============================================================================
# Steuert, welche Konfigurationsquelle Vorrang hat:
# - 'database' (Standard): Werte aus Admin-Settings überschreiben .env Werte
# - 'env': .env Werte überschreiben Admin-Settings
# Nützlich für Test-Server, wo du .env Werte verwenden möchtest,
# ohne die Datenbank-Settings ändern zu müssen
DHL_CONFIG_SOURCE=env
# DHL API Zugangsdaten (konsolidiert)
DHL_SANDBOX_URL=https://api-sandbox.dhl.com
DHL_BASE_URL=https://api-eu.dhl.com
DHL_SANDBOX=false
DHL_TEST_MODE=false
DHL_API_KEY=AxGBdF8DBdIAmuhqvG0ASBRKFvyV7ypX
# DHL Standard-Einstellungen
DHL_PRODUCT=V01PAK
DHL_LABEL_FORMAT=PDF
DHL_PRINT_FORMAT=910-300-710
DHL_RETOURE_PRINT_FORMAT=910-300-700
DHL_PROFILE=STANDARD_GRUPPENPROFIL
# DHL Queue Configuration
# Set to true for background processing (requires queue worker)
# Set to false for immediate processing
DHL_USE_QUEUE=false
# DHL Account Numbers (für verschiedene Produkte)
DHL_USERNAME=riwa-tec
DHL_PASSWORD=MivitaCare!!2025
DHL_BILLING_NUMBER=63144073550101
DHL_ACCOUNT_NUMBER_DEFAULT=63144073550101
DHL_ACCOUNT_NUMBER_V01PAK=63144073550101 # DHL Paket National
DHL_ACCOUNT_NUMBER_V62WP=63144073556201 # Warenpost National
DHL_ACCOUNT_NUMBER_V53PAK=63144073555301 # DHL Paket International
DHL_ACCOUNT_NUMBER_V07PAK=63144073550701 # DHL Retoure Online
#sandbox
#DHL_USERNAME=user-valid
#DHL_PASSWORD=SandboxPasswort2023!
#DHL_BILLING_NUMBER=33333333330101
#DHL_ACCOUNT_NUMBER_DEFAULT=33333333330101
#DHL_ACCOUNT_NUMBER_V01PAK=33333333330102 # DHL Paket National
#DHL_ACCOUNT_NUMBER_V62WP=33333333336601 # Warenpost National
#DHL_ACCOUNT_NUMBER_V53PAK=33333333335301 # DHL Paket International
#DHL_ACCOUNT_NUMBER_V07PAK=33333333330702 # DHL Retoure Online
#V66WPI|33333333336601
#VO1PAK 33333333330102 33333333330101
#V53WPAK 33333333335301
#VO7PAK 33333333330702
# DHL Absenderadresse
DHL_SENDER_COMPANY="mivita care gmbh"
DHL_SENDER_NAME=""
DHL_SENDER_STREET="Leinfeld"
DHL_SENDER_STREET_NUMBER=2
DHL_SENDER_POSTAL_CODE=87755
DHL_SENDER_CITY=Kirchhaslach
DHL_SENDER_COUNTRY=DE
DHL_SENDER_EMAIL=versand@mivita.care
DHL_SENDER_PHONE="+49 123 456789"
# DHL Legacy/Compatibility Settings
DHL_API_TYPE=developer
DHL_API_SECRET=OyoeePEbYmY1EuOG
MIVITA_ADD_NUMBER_ID=946

View file

@ -1,74 +0,0 @@
# =============================================================================
# DHL VERSANDMODUL KONFIGURATION - BEISPIEL
# =============================================================================
# Diese Datei enthält alle DHL-spezifischen Environment-Variablen
# Kopieren Sie die benötigten Variablen in Ihre .env Datei
# DHL API Zugangsdaten
# Option 1: DHL Developer API (developer.dhl.com) - Empfohlen
DHL_API_KEY=your_api_key_from_developer_dhl_com
DHL_API_SECRET=your_api_secret_from_developer_dhl_com
# Option 2: DHL Business Customer API (Klassisch)
DHL_API_USERNAME=your_dhl_username
DHL_API_PASSWORD=your_dhl_password
DHL_ACCOUNT_NUMBER=your_14_digit_account_number
# API-Konfiguration
DHL_SANDBOX=true # false für Produktion
DHL_TEST_MODE=true # false für Produktion
DHL_API_TYPE=developer # 'developer' oder 'business_customer'
# Absenderadresse (Ihre Firmenadresse)
DHL_SENDER_COMPANY="Ihr Firmenname"
DHL_SENDER_NAME="Versandabteilung"
DHL_SENDER_STREET="Ihre Straße"
DHL_SENDER_STREET_NUMBER=123
DHL_SENDER_POSTAL_CODE=12345
DHL_SENDER_CITY="Ihre Stadt"
DHL_SENDER_STATE=
DHL_SENDER_COUNTRY=DE
DHL_SENDER_EMAIL=versand@ihrefirma.de
DHL_SENDER_PHONE="+49 123 456789"
# Standard-Einstellungen
DHL_DEFAULT_PRODUCT=V01PAK # V01PAK = DHL Paket
DHL_LABEL_FORMAT=PDF # PDF oder ZPL
DHL_LABEL_SIZE=A4 # A4 oder 10x15cm
DHL_LABEL_STORAGE_PATH=storage/app/dhl/labels
DHL_LABEL_PUBLIC_PATH=dhl/labels
# Retouren
DHL_RETURNS_ENABLED=true
DHL_RETURN_PRODUCT=V07PAK # V07PAK = DHL Paket Return
DHL_RETURN_RECEIVER_ID=your_return_receiver_id
# Tracking
DHL_TRACKING_ENABLED=true
DHL_TRACKING_AUTO_UPDATE=true
DHL_TRACKING_UPDATE_INTERVAL=60 # Minuten
# Logging
DHL_LOGGING_ENABLED=true
DHL_LOGGING_LEVEL=info # debug, info, warning, error
DHL_LOG_REQUESTS=true
DHL_LOG_RESPONSES=true
# Queue (verwende bestehende Queue-Konfiguration)
DHL_QUEUE_CONNECTION=database
DHL_QUEUE_NAME=dhl
DHL_QUEUE_TIMEOUT=60
DHL_QUEUE_MAX_TRIES=3
# Fehlerbehandlung
DHL_THROW_EXCEPTIONS=true
DHL_ERROR_NOTIFICATION_EMAIL=admin@ihrefirma.de
# Cache
DHL_CACHE_ENABLED=true
DHL_CACHE_TTL=3600 # Sekunden
# Entwicklung/Testing
DHL_FAKE_API_CALLS=false # true um API-Calls zu simulieren
DHL_MOCK_RESPONSES=false # true um Mock-Responses zu verwenden
DHL_DEBUG_MODE=false # true für detaillierte Debug-Informationen

View file

@ -1,89 +0,0 @@
# =============================================================================
# DHL VERSANDMODUL KONFIGURATION - BEISPIEL
# =============================================================================
# Diese Datei enthält alle DHL-spezifischen Environment-Variablen
# Kopieren Sie die benötigten Variablen in Ihre .env Datei
# =============================================================================
# DHL KONFIGURATION - PRIORITÄT
# =============================================================================
# Steuert, welche Konfigurationsquelle Vorrang hat:
# - 'database' (Standard): Werte aus Admin-Settings überschreiben .env Werte
# - 'env': .env Werte überschreiben Admin-Settings
#
# Nützlich für Test-Server, wo du .env Werte verwenden möchtest,
# ohne die Datenbank-Settings ändern zu müssen
DHL_CONFIG_SOURCE=database
# DHL API Zugangsdaten
# Option 1: DHL Developer API (developer.dhl.com) - Empfohlen
DHL_API_KEY=your_api_key_from_developer_dhl_com
DHL_API_SECRET=your_api_secret_from_developer_dhl_com
# Option 2: DHL Business Customer API (Klassisch)
DHL_API_USERNAME=your_dhl_username
DHL_API_PASSWORD=your_dhl_password
DHL_ACCOUNT_NUMBER=your_14_digit_account_number
# API-Konfiguration
DHL_SANDBOX=true # false für Produktion
DHL_TEST_MODE=true # false für Produktion
DHL_API_TYPE=developer # 'developer' oder 'business_customer'
# Absenderadresse (Ihre Firmenadresse)
DHL_SENDER_COMPANY="Ihr Firmenname"
DHL_SENDER_NAME="Versandabteilung"
DHL_SENDER_STREET="Ihre Straße"
DHL_SENDER_STREET_NUMBER=123
DHL_SENDER_POSTAL_CODE=12345
DHL_SENDER_CITY="Ihre Stadt"
DHL_SENDER_STATE=
DHL_SENDER_COUNTRY=DE
DHL_SENDER_EMAIL=versand@ihrefirma.de
DHL_SENDER_PHONE="+49 123 456789"
# Standard-Einstellungen
DHL_DEFAULT_PRODUCT=V01PAK # V01PAK = DHL Paket
DHL_LABEL_FORMAT=PDF # PDF oder ZPL
DHL_LABEL_SIZE=A4 # A4 oder 10x15cm
DHL_LABEL_STORAGE_PATH=storage/app/dhl/labels
DHL_LABEL_PUBLIC_PATH=dhl/labels
# Retouren
DHL_RETURNS_ENABLED=true
DHL_RETURN_PRODUCT=V07PAK # V07PAK = DHL Paket Return
DHL_RETURN_RECEIVER_ID=your_return_receiver_id
# Tracking
DHL_TRACKING_ENABLED=true
DHL_TRACKING_AUTO_UPDATE=true
DHL_TRACKING_UPDATE_INTERVAL=60 # Minuten
# Logging
DHL_LOGGING_ENABLED=true
DHL_LOGGING_LEVEL=info # debug, info, warning, error
DHL_LOG_REQUESTS=true
DHL_LOG_RESPONSES=true
# Queue (verwende bestehende Queue-Konfiguration)
DHL_QUEUE_CONNECTION=database
DHL_QUEUE_NAME=dhl
DHL_QUEUE_TIMEOUT=60
DHL_QUEUE_MAX_TRIES=3
# Fehlerbehandlung
DHL_THROW_EXCEPTIONS=true
DHL_ERROR_NOTIFICATION_EMAIL=admin@ihrefirma.de
# Cache
DHL_CACHE_ENABLED=true
DHL_CACHE_TTL=3600 # Sekunden
# Entwicklung/Testing
DHL_FAKE_API_CALLS=false # true um API-Calls zu simulieren
DHL_MOCK_RESPONSES=false # true um Mock-Responses zu verwenden
DHL_DEBUG_MODE=false # true für detaillierte Debug-Informationen
# Business Commands Configuration
# Erlaubt Business-Commands an jedem Tag (nur für Test-Server!)
# Auf Live-Server muss dies false sein oder weggelassen werden
BUSINESS_FORCE_EXECUTE=false

59
.gitignore vendored
View file

@ -1,42 +1,16 @@
# Laravel
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/public/vendor
/storage/*.key
/storage/app
/storage/framework
/storage/language
/storage/logs
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
# IDEs & Editors
/.fleet
/.idea
/.nova
/.vscode
/.zed
.claude/
.cursor/
# macOS
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
@ -44,15 +18,18 @@ yarn-error.log
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
Icon
# Project specific
_static/
_work/
_storage/
/vendor
/node_modules
/storage/language
/storage/framework
/storage/logs
/public/vendor
/storage/app

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Datasource local storage ignored files
/dataSources.local.xml
/dataSources/

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="framework" type="frameworkType"/>
<xs:complexType name="commandType">
<xs:all>
<xs:element type="xs:string" name="name" minOccurs="1" maxOccurs="1"/>
<xs:element type="xs:string" name="params" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
<xs:element type="optionsBeforeType" name="optionsBefore" minOccurs="0" maxOccurs="1"/>
</xs:all>
</xs:complexType>
<xs:complexType name="frameworkType">
<xs:sequence>
<xs:element type="xs:string" name="extraData" minOccurs="0" maxOccurs="1"/>
<xs:element type="commandType" name="command" maxOccurs="unbounded" minOccurs="0"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="invoke" use="required"/>
<xs:attribute type="xs:string" name="alias" use="required"/>
<xs:attribute type="xs:boolean" name="enabled" use="required"/>
<xs:attribute type="xs:integer" name="version" use="required"/>
<xs:attribute type="xs:string" name="frameworkId" use="optional"/>
</xs:complexType>
<xs:complexType name="optionsBeforeType">
<xs:sequence>
<xs:element type="optionType" name="option" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="optionType">
<xs:sequence>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="shortcut" use="optional"/>
<xs:attribute name="pattern" use="optional">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="space"/>
<xs:enumeration value="equals"/>
<xs:enumeration value="unknown"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:schema>

10
.idea/composerJson.xml generated Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerJsonPluginSettings">
<unboundedVersionInspectionSettings>
<excludedPackages />
</unboundedVersionInspectionSettings>
<customRepositories />
<composerUpdateOptions />
</component>
</project>

11
.idea/dataSources.xml generated Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="MySQL - @localhost" uuid="f58b2045-72b3-4199-b31f-5c9c653c31de">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/mivita</jdbc-url>
</data-source>
</component>
</project>

4
.idea/deployment.xml generated Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" serverName="my.mivita.test" />
</project>

6
.idea/encodings.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

85
.idea/jsLinters/jshint.xml generated Normal file
View file

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JSHintConfiguration" version="2.9.5" use-config-file="true" use-custom-config-file="true" custom-config-file-path="$PROJECT_DIR$/assets/plugins/footable/js/.jshintrc">
<option asi="false" />
<option bitwise="true" />
<option boss="false" />
<option browser="true" />
<option browserify="false" />
<option camelcase="false" />
<option couch="false" />
<option curly="true" />
<option debug="false" />
<option devel="false" />
<option dojo="false" />
<option elision="false" />
<option enforceall="false" />
<option eqeqeq="true" />
<option eqnull="false" />
<option es3="false" />
<option es5="false" />
<option esnext="false" />
<option evil="false" />
<option expr="false" />
<option forin="true" />
<option freeze="false" />
<option funcscope="false" />
<option futurehostile="false" />
<option gcl="false" />
<option globalstrict="false" />
<option immed="false" />
<option iterator="false" />
<option jasmine="false" />
<option jquery="false" />
<option lastsemic="false" />
<option latedef="false" />
<option laxbreak="false" />
<option laxcomma="false" />
<option loopfunc="false" />
<option maxerr="50" />
<option mocha="false" />
<option module="false" />
<option mootools="false" />
<option moz="false" />
<option multistr="false" />
<option newcap="false" />
<option noarg="true" />
<option nocomma="false" />
<option node="false" />
<option noempty="true" />
<option nomen="false" />
<option nonbsp="false" />
<option nonew="true" />
<option nonstandard="false" />
<option notypeof="false" />
<option noyield="false" />
<option onevar="false" />
<option passfail="false" />
<option phantom="false" />
<option plusplus="false" />
<option proto="false" />
<option prototypejs="false" />
<option qunit="false" />
<option quotmark="false" />
<option rhino="false" />
<option scripturl="false" />
<option shadow="false" />
<option shelljs="false" />
<option singleGroups="false" />
<option smarttabs="false" />
<option strict="true" />
<option sub="false" />
<option supernew="false" />
<option trailing="false" />
<option typed="false" />
<option undef="true" />
<option unused="false" />
<option validthis="false" />
<option varstmt="false" />
<option white="false" />
<option withstmt="false" />
<option worker="false" />
<option wsh="false" />
<option yui="false" />
</component>
</project>

6
.idea/laravel-plugin.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="LaravelPluginSettings">
<option name="pluginEnabled" value="true" />
</component>
</project>

6
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

147
.idea/mivita.care.iml generated Normal file
View file

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="App\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/askedio/laravel5-profanity-filter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-dompdf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-ide-helper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/cocur/slugify" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/cviebrock/eloquent-sluggable" />
<excludeFolder url="file://$MODULE_DIR$/vendor/defuse/php-encryption" />
<excludeFolder url="file://$MODULE_DIR$/vendor/dnoegel/php-xdg-base-dir" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/dompdf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/dragonmantank/cron-expression" />
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fideloper/proxy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/filp/whoops" />
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fzaninotto/faker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/hamcrest/hamcrest-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/intervention/image" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jakub-onderka/php-console-color" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jakub-onderka/php-console-highlighter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jenssegers/date" />
<excludeFolder url="file://$MODULE_DIR$/vendor/justinrainbow/json-schema" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laminas/laminas-diactoros" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laminas/laminas-zendframework-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laracasts/flash" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/framework" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/helpers" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/passport" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/tinker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravelcollective/html" />
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/commonmark" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/event" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/flysystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/oauth2-server" />
<excludeFolder url="file://$MODULE_DIR$/vendor/maatwebsite/excel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/complex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/matrix" />
<excludeFolder url="file://$MODULE_DIR$/vendor/maximebf/debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mockery/mockery" />
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nunomaduro/collision" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nyholm/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/olimortimer/laravelshoppingcart" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/closure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/paragonie/random_compat" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phenx/php-font-lib" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phenx/php-svg-lib" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/message-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpseclib/phpseclib" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpspec/prophecy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-token-stream" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psy/psysh" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/reliese/laravel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/resource-operations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/seld/jsonlint" />
<excludeFolder url="file://$MODULE_DIR$/vendor/seld/phar-utils" />
<excludeFolder url="file://$MODULE_DIR$/vendor/setasign/fpdf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/setasign/fpdi" />
<excludeFolder url="file://$MODULE_DIR$/vendor/swiftmailer/swiftmailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-iconv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php72" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php73" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/psr-http-message-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/yajra/laravel-datatables-oracle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mivita.care.iml" filepath="$PROJECT_DIR$/.idea/mivita.care.iml" />
</modules>
</component>
</project>

14
.idea/php-test-framework.xml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="Local" version="9.1.4" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

148
.idea/php.xml generated Normal file
View file

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/mockery/mockery" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/seld/phar-utils" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-token-stream" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/barryvdh/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-ide-helper" />
<path value="$PROJECT_DIR$/vendor/phpspec/prophecy" />
<path value="$PROJECT_DIR$/vendor/jakub-onderka/php-console-highlighter" />
<path value="$PROJECT_DIR$/vendor/jenssegers/date" />
<path value="$PROJECT_DIR$/vendor/jakub-onderka/php-console-color" />
<path value="$PROJECT_DIR$/vendor/league/flysystem" />
<path value="$PROJECT_DIR$/vendor/fideloper/proxy" />
<path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
<path value="$PROJECT_DIR$/vendor/psy/psysh" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/symfony/debug" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/seld/jsonlint" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php72" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/swiftmailer/swiftmailer" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/justinrainbow/json-schema" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/paragonie/random_compat" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/dnoegel/php-xdg-base-dir" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/laravel/tinker" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/laravel/framework" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/laravelcollective/html" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/filp/whoops" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/intervention/image" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/yajra/laravel-datatables-oracle" />
<path value="$PROJECT_DIR$/vendor/fzaninotto/faker" />
<path value="$PROJECT_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/ramsey/uuid" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/nunomaduro/collision" />
<path value="$PROJECT_DIR$/vendor/nesbot/carbon" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/hamcrest/hamcrest-php" />
<path value="$PROJECT_DIR$/vendor/dragonmantank/cron-expression" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/cviebrock/eloquent-sluggable" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/askedio/laravel5-profanity-filter" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-iconv" />
<path value="$PROJECT_DIR$/vendor/reliese/laravel" />
<path value="$PROJECT_DIR$/vendor/setasign/fpdf" />
<path value="$PROJECT_DIR$/vendor/setasign/fpdi" />
<path value="$PROJECT_DIR$/vendor/olimortimer/laravelshoppingcart" />
<path value="$PROJECT_DIR$/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/vendor/opis/closure" />
<path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
<path value="$PROJECT_DIR$/vendor/maximebf/debugbar" />
<path value="$PROJECT_DIR$/vendor/laracasts/flash" />
<path value="$PROJECT_DIR$/vendor/cocur/slugify" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-debugbar" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
<path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" />
<path value="$PROJECT_DIR$/vendor/laminas/laminas-zendframework-bridge" />
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
<path value="$PROJECT_DIR$/vendor/php-http/message-factory" />
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/laravel/passport" />
<path value="$PROJECT_DIR$/vendor/laravel/helpers" />
<path value="$PROJECT_DIR$/vendor/nyholm/psr7" />
<path value="$PROJECT_DIR$/vendor/phenx/php-font-lib" />
<path value="$PROJECT_DIR$/vendor/phenx/php-svg-lib" />
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
<path value="$PROJECT_DIR$/vendor/phpseclib/phpseclib" />
<path value="$PROJECT_DIR$/vendor/league/event" />
<path value="$PROJECT_DIR$/vendor/league/oauth2-server" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-dompdf" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/maatwebsite/excel" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.2" />
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
</project>

8
.idea/symfony2.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Symfony2PluginSettings">
<option name="directoryToWeb" value="public" />
<option name="pluginEnabled" value="true" />
<option name="lastServiceGeneratorLanguage" value="yaml" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

627
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,627 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="2fbaac5f-25ba-4502-a970-cc14728d7d55" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/resources/views/user/shop/sales/order_detail.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Http/Controllers/Api/ShoppingUserController.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Http/Controllers/Api/ShoppingUserController.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Http/Controllers/ProductController.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Http/Controllers/ProductController.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Http/Controllers/SalesController.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Http/Controllers/SalesController.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Http/Controllers/Web/ContactController.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Http/Controllers/Web/ContactController.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Mail/MailCheckout.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Mail/MailCheckout.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Models/ShippingPrice.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Models/ShippingPrice.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Models/ShoppingOrder.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Models/ShoppingOrder.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Models/ShoppingUser.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Models/ShoppingUser.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/Repositories/ProductRepository.php" beforeDir="false" afterPath="$PROJECT_DIR$/app/Repositories/ProductRepository.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/cart.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/cart.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/mail.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/mail.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/database/migrations/2019_02_23_163527_create_shopping_orders_table.php" beforeDir="false" afterPath="$PROJECT_DIR$/database/migrations/2019_02_23_163527_create_shopping_orders_table.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/lang/de Kopie.json" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/resources/lang/de/email.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/lang/de/email.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/lang/de/register.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/lang/de/register.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/admin/product/index.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/admin/product/index.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/admin/sales/_detail.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/admin/sales/_detail.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/emails/checkout.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/emails/checkout.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/layouts/application.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/layouts/application.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/user/customer/detail.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/user/customer/detail.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/views/web/templates/registrierung.blade.php" beforeDir="false" afterPath="$PROJECT_DIR$/resources/views/web/templates/registrierung.blade.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/routes/api.php" beforeDir="false" afterPath="$PROJECT_DIR$/routes/api.php" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ComposerSettings" doNotAsk="true" synchronizationState="SYNCHRONIZE">
<pharConfigPath>$PROJECT_DIR$/composer.json</pharConfigPath>
<execution>
<phar pharPath="/usr/local/bin/composer" interpreterId="1b9e99f0-0fb5-45bb-9686-0d9b61bf499b" />
</execution>
</component>
<component name="DatabaseView">
<option name="SHOW_INTERMEDIATE" value="true" />
<option name="GROUP_DATA_SOURCES" value="true" />
<option name="GROUP_SCHEMA" value="true" />
<option name="GROUP_CONTENTS" value="false" />
<option name="SORT_POSITIONED" value="false" />
<option name="SHOW_EMPTY_GROUPS" value="false" />
<option name="AUTO_SCROLL_FROM_SOURCE" value="false" />
<option name="HIDDEN_KINDS">
<set />
</option>
<expand>
<path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="MySQL - @localhost" type="feb32156:DbDataSourceImpl" />
</path>
<path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="MySQL - @localhost" type="feb32156:DbDataSourceImpl" />
<item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" />
</path>
<path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="MySQL - @localhost" type="feb32156:DbDataSourceImpl" />
<item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" />
<item name="mivita: schema" type="76f4a085:MysqlImplModel$Schema" />
</path>
<path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="MySQL - @localhost" type="feb32156:DbDataSourceImpl" />
<item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" />
<item name="mivita: schema" type="76f4a085:MysqlImplModel$Schema" />
<item name="user_histories: table" type="285a2a93:MysqlImplModel$Table" />
</path>
</expand>
<select />
</component>
<component name="FrameworkCommandLineHistory">
<commandsHistory>
<command text="php artisan ide-helper:meta" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:generate" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:meta" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:generate" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:generate" />
<command text="composer dump-autoload" />
<command text="composer run-script post-update-cmd" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:meta" />
<command text="php artisan ide-helper:generate" />
<command text="php artisan ide-helper:meta" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:generate" />
<command text="composer dump-autoload" />
<command text="composer run-script post-update-cmd" />
<command text="php artisan ide-helper:generate" />
<command text="composer run-script post-update-cmd" />
<command text="php artisan ide-helper:models" />
<command text="php artisan ide-helper:meta" />
<command text="php artisan ide-helper:generate" />
<command text="composer run-script post-update-cmd" />
</commandsHistory>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="PhpServers">
<servers />
</component>
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 7.1">
<include_path>
<path value="$PROJECT_DIR$/vendor/mockery/mockery" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/seld/phar-utils" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-token-stream" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/barryvdh/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-ide-helper" />
<path value="$PROJECT_DIR$/vendor/phpspec/prophecy" />
<path value="$PROJECT_DIR$/vendor/jakub-onderka/php-console-highlighter" />
<path value="$PROJECT_DIR$/vendor/jenssegers/date" />
<path value="$PROJECT_DIR$/vendor/jakub-onderka/php-console-color" />
<path value="$PROJECT_DIR$/vendor/league/flysystem" />
<path value="$PROJECT_DIR$/vendor/fideloper/proxy" />
<path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
<path value="$PROJECT_DIR$/vendor/psy/psysh" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/symfony/debug" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/seld/jsonlint" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php72" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/swiftmailer/swiftmailer" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/justinrainbow/json-schema" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/paragonie/random_compat" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/dnoegel/php-xdg-base-dir" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/laravel/tinker" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/laravel/framework" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/laravelcollective/html" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/filp/whoops" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/vendor/intervention/image" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/yajra/laravel-datatables-oracle" />
<path value="$PROJECT_DIR$/vendor/fzaninotto/faker" />
<path value="$PROJECT_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/ramsey/uuid" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/nunomaduro/collision" />
<path value="$PROJECT_DIR$/vendor/nesbot/carbon" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/hamcrest/hamcrest-php" />
<path value="$PROJECT_DIR$/vendor/dragonmantank/cron-expression" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/cviebrock/eloquent-sluggable" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/askedio/laravel5-profanity-filter" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-iconv" />
<path value="$PROJECT_DIR$/vendor/reliese/laravel" />
<path value="$PROJECT_DIR$/vendor/setasign/fpdf" />
<path value="$PROJECT_DIR$/vendor/setasign/fpdi" />
<path value="$PROJECT_DIR$/vendor/olimortimer/laravelshoppingcart" />
<path value="$PROJECT_DIR$/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/vendor/opis/closure" />
<path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
<path value="$PROJECT_DIR$/vendor/maximebf/debugbar" />
<path value="$PROJECT_DIR$/vendor/laracasts/flash" />
<path value="$PROJECT_DIR$/vendor/cocur/slugify" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-debugbar" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
<path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" />
<path value="$PROJECT_DIR$/vendor/laminas/laminas-zendframework-bridge" />
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
<path value="$PROJECT_DIR$/vendor/php-http/message-factory" />
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/laravel/passport" />
<path value="$PROJECT_DIR$/vendor/laravel/helpers" />
<path value="$PROJECT_DIR$/vendor/nyholm/psr7" />
<path value="$PROJECT_DIR$/vendor/phenx/php-font-lib" />
<path value="$PROJECT_DIR$/vendor/phenx/php-svg-lib" />
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
<path value="$PROJECT_DIR$/vendor/phpseclib/phpseclib" />
<path value="$PROJECT_DIR$/vendor/league/event" />
<path value="$PROJECT_DIR$/vendor/league/oauth2-server" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-dompdf" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/maatwebsite/excel" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
</include_path>
</component>
<component name="ProjectId" id="1Ply2ASLtUbE38LpjzejPOnh0np" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="ASKED_ADD_EXTERNAL_FILES" value="true" />
<property name="DatabaseDriversLRU" value="mysql" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/resources/views/user/shop/sales" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
<property name="nodejs_package_manager_path" value="npm" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
<property name="vue.rearranger.settings.migration" value="true" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/resources/views/user/shop/sales" />
<recent name="$PROJECT_DIR$/resources/views/emails" />
<recent name="$PROJECT_DIR$/resources/views/admin/sales" />
<recent name="$PROJECT_DIR$/resources/views/user/homeparty" />
<recent name="$PROJECT_DIR$/app/Services" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/resources/views/user/order" />
<recent name="$PROJECT_DIR$/resources/views/user/customer" />
<recent name="$PROJECT_DIR$/public/vendor/libs/dropzone" />
<recent name="$PROJECT_DIR$/public/vendor/libs" />
<recent name="$PROJECT_DIR$/resources/views/admin/user" />
</key>
</component>
<component name="RunManager" selected="Shell Script.composer-post-update.sh">
<configuration name="de" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
<TestRunner directory="$PROJECT_DIR$/resources/lang/de" />
<method v="2" />
</configuration>
<configuration default="true" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$USER_HOME$/Documents/scripts/composer-post-update.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<envs />
<method v="2" />
</configuration>
<configuration name="composer-post-update.sh" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$USER_HOME$/Documents/scripts/composer-post-update.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="2fbaac5f-25ba-4502-a970-cc14728d7d55" name="Default Changelist" comment="" />
<created>1537353993493</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1537353993493</updated>
<workItem from="1537353995669" duration="3800000" />
<workItem from="1540127177718" duration="362000" />
<workItem from="1540127554029" duration="2000" />
<workItem from="1540127581792" duration="7784000" />
<workItem from="1542210149879" duration="9736000" />
<workItem from="1542297086254" duration="9567000" />
<workItem from="1542406540801" duration="9615000" />
<workItem from="1542727683895" duration="12063000" />
<workItem from="1542787061822" duration="2626000" />
<workItem from="1542806743382" duration="5489000" />
<workItem from="1542890756312" duration="12280000" />
<workItem from="1543070248579" duration="2602000" />
<workItem from="1543233637816" duration="18000" />
<workItem from="1544822014608" duration="4694000" />
<workItem from="1544829993268" duration="5406000" />
<workItem from="1546523595056" duration="30055000" />
<workItem from="1546704135920" duration="19864000" />
<workItem from="1546806383318" duration="9637000" />
<workItem from="1547205813997" duration="946000" />
<workItem from="1547208157542" duration="108000" />
<workItem from="1547214977808" duration="3425000" />
<workItem from="1547480630058" duration="1000" />
<workItem from="1547484887720" duration="1609000" />
<workItem from="1547540787341" duration="1640000" />
<workItem from="1547724573711" duration="736000" />
<workItem from="1548058316947" duration="24674000" />
<workItem from="1548143988826" duration="810000" />
<workItem from="1548231704434" duration="1070000" />
<workItem from="1548773013181" duration="1000" />
<workItem from="1549468946720" duration="8000" />
<workItem from="1550313918368" duration="329000" />
<workItem from="1550314558764" duration="1097000" />
<workItem from="1550498022950" duration="3845000" />
<workItem from="1550674465433" duration="14138000" />
<workItem from="1550736184159" duration="1000" />
<workItem from="1550765848683" duration="10855000" />
<workItem from="1550838986113" duration="11950000" />
<workItem from="1550935201261" duration="6226000" />
<workItem from="1551181680267" duration="14089000" />
<workItem from="1551261197445" duration="32000" />
<workItem from="1551277131077" duration="11095000" />
<workItem from="1551354000907" duration="15584000" />
<workItem from="1551429281485" duration="275000" />
<workItem from="1551429580227" duration="2056000" />
<workItem from="1551436264383" duration="33071000" />
<workItem from="1551522312534" duration="3206000" />
<workItem from="1551526048926" duration="2305000" />
<workItem from="1551530777330" duration="3050000" />
<workItem from="1551712620909" duration="2498000" />
<workItem from="1551962594785" duration="3948000" />
<workItem from="1552133587503" duration="961000" />
<workItem from="1552134613344" duration="6000" />
<workItem from="1552135056549" duration="494000" />
<workItem from="1552403932079" duration="1027000" />
<workItem from="1552405331969" duration="881000" />
<workItem from="1552473972131" duration="2992000" />
<workItem from="1552477317174" duration="722000" />
<workItem from="1552481695438" duration="231000" />
<workItem from="1552482684973" duration="1481000" />
<workItem from="1552485686862" duration="497000" />
<workItem from="1553078315005" duration="42000" />
<workItem from="1553172339375" duration="754000" />
<workItem from="1553174858066" duration="22000" />
<workItem from="1553175700548" duration="116000" />
<workItem from="1553176149267" duration="34000" />
<workItem from="1553687920206" duration="395000" />
<workItem from="1553692408844" duration="6000" />
<workItem from="1553694116510" duration="350000" />
<workItem from="1553694855934" duration="1157000" />
<workItem from="1553708521909" duration="492000" />
<workItem from="1555429292695" duration="630000" />
<workItem from="1555490678820" duration="3921000" />
<workItem from="1556891567027" duration="851000" />
<workItem from="1560255951776" duration="539000" />
<workItem from="1561993809455" duration="29000" />
<workItem from="1566462771075" duration="8798000" />
<workItem from="1566975548985" duration="1435000" />
<workItem from="1568115248448" duration="7659000" />
<workItem from="1573475266384" duration="174000" />
<workItem from="1573475447829" duration="81000" />
<workItem from="1573480792424" duration="1748000" />
<workItem from="1573806280308" duration="772000" />
<workItem from="1574173803219" duration="19000" />
<workItem from="1574495107293" duration="2126000" />
<workItem from="1574498597084" duration="18390000" />
<workItem from="1574870171463" duration="442000" />
<workItem from="1575135233364" duration="233000" />
<workItem from="1577966622446" duration="15215000" />
<workItem from="1578042899454" duration="15408000" />
<workItem from="1578069761418" duration="55000" />
<workItem from="1578070274627" duration="66000" />
<workItem from="1578493675622" duration="10000" />
<workItem from="1578493690816" duration="11299000" />
<workItem from="1578672302747" duration="1742000" />
<workItem from="1578831425036" duration="6019000" />
<workItem from="1578928076715" duration="559000" />
<workItem from="1579028224750" duration="42000" />
<workItem from="1580139983493" duration="1049000" />
<workItem from="1581156718221" duration="27110000" />
<workItem from="1581245547669" duration="5492000" />
<workItem from="1581502538849" duration="2174000" />
<workItem from="1581517312628" duration="9759000" />
<workItem from="1581604452585" duration="5853000" />
<workItem from="1581686945791" duration="4486000" />
<workItem from="1581691515338" duration="19033000" />
<workItem from="1582045337297" duration="7000" />
<workItem from="1582975911467" duration="18419000" />
<workItem from="1583498764097" duration="142000" />
<workItem from="1583572250584" duration="19295000" />
<workItem from="1583743438432" duration="10157000" />
<workItem from="1583757303144" duration="7000" />
<workItem from="1583758271548" duration="2118000" />
<workItem from="1584782149779" duration="14772000" />
<workItem from="1584954914592" duration="54000" />
<workItem from="1584965547046" duration="694000" />
<workItem from="1585396827913" duration="1986000" />
<workItem from="1585400638388" duration="9592000" />
<workItem from="1585576417290" duration="2940000" />
<workItem from="1585666412873" duration="6324000" />
<workItem from="1585729859738" duration="21452000" />
<workItem from="1585760254632" duration="571000" />
<workItem from="1587107320088" duration="1592000" />
<workItem from="1587109717986" duration="1617000" />
<workItem from="1587749609220" duration="1211000" />
<workItem from="1587975636191" duration="17000" />
<workItem from="1588146697363" duration="6072000" />
<workItem from="1588153881658" duration="6306000" />
<workItem from="1588167994966" duration="10515000" />
<workItem from="1588231371983" duration="3000" />
<workItem from="1588233321074" duration="6060000" />
<workItem from="1588244040497" duration="547000" />
<workItem from="1588244809449" duration="10424000" />
<workItem from="1588267918069" duration="1856000" />
<workItem from="1588410019835" duration="194000" />
<workItem from="1588411442653" duration="11498000" />
<workItem from="1588427157422" duration="5582000" />
<workItem from="1588577728345" duration="8457000" />
<workItem from="1588597117898" duration="3955000" />
<workItem from="1588604782047" duration="6478000" />
<workItem from="1588667412409" duration="3521000" />
<workItem from="1588753513112" duration="331000" />
<workItem from="1588754727146" duration="6339000" />
<workItem from="1588761249067" duration="27000" />
<workItem from="1588761617312" duration="1790000" />
<workItem from="1588771915210" duration="5000" />
<workItem from="1588778353962" duration="35000" />
<workItem from="1588779256522" duration="57000" />
<workItem from="1588921253853" duration="2440000" />
<workItem from="1589023916958" duration="245000" />
<workItem from="1589355379810" duration="20000" />
<workItem from="1589365636618" duration="4232000" />
<workItem from="1589442905154" duration="21886000" />
<workItem from="1589474488991" duration="3220000" />
<workItem from="1589526578461" duration="28000" />
<workItem from="1589881416788" duration="7714000" />
<workItem from="1589891829781" duration="681000" />
<workItem from="1589892673376" duration="9169000" />
<workItem from="1589902453020" duration="273000" />
<workItem from="1589974449247" duration="240000" />
<workItem from="1590758073413" duration="5869000" />
<workItem from="1590826972537" duration="2356000" />
<workItem from="1590829892402" duration="8158000" />
<workItem from="1591187714339" duration="7341000" />
<workItem from="1591361845658" duration="2000" />
<workItem from="1591619467115" duration="326000" />
<workItem from="1591864110935" duration="4671000" />
<workItem from="1591868858012" duration="15798000" />
<workItem from="1591889648864" duration="8510000" />
<workItem from="1591959307471" duration="5000" />
<workItem from="1591959326440" duration="5517000" />
<workItem from="1592132838802" duration="47000" />
<workItem from="1592134604562" duration="347000" />
<workItem from="1592134995936" duration="7000" />
<workItem from="1592135066330" duration="179000" />
<workItem from="1592831744708" duration="9000" />
<workItem from="1592831761021" duration="3000" />
<workItem from="1592905155133" duration="2081000" />
<workItem from="1593539130650" duration="5379000" />
<workItem from="1593607978036" duration="4673000" />
<workItem from="1594295945600" duration="612000" />
<workItem from="1594721064087" duration="5912000" />
<workItem from="1594733233309" duration="1305000" />
<workItem from="1595674257808" duration="9321000" />
<workItem from="1595923785716" duration="1023000" />
<workItem from="1595948227650" duration="1138000" />
<workItem from="1595949374319" duration="4908000" />
<workItem from="1596107641527" duration="35000" />
<workItem from="1596107688024" duration="1943000" />
<workItem from="1596111667880" duration="6240000" />
<workItem from="1596119318253" duration="10939000" />
<workItem from="1596188575338" duration="7868000" />
<workItem from="1596268902765" duration="165000" />
<workItem from="1596269074069" duration="69000" />
<workItem from="1596802450904" duration="6190000" />
<workItem from="1596873885106" duration="22962000" />
<workItem from="1597138806746" duration="5000" />
<workItem from="1597216110022" duration="32246000" />
<workItem from="1597299830339" duration="2158000" />
<workItem from="1597313688277" duration="5656000" />
<workItem from="1597410738317" duration="11162000" />
<workItem from="1597658985953" duration="2000" />
<workItem from="1597928402978" duration="7140000" />
<workItem from="1598016608485" duration="9648000" />
<workItem from="1598255562481" duration="22043000" />
<workItem from="1598282618629" duration="1154000" />
<workItem from="1598434849256" duration="514000" />
<workItem from="1598971116331" duration="4130000" />
<workItem from="1599033832974" duration="4000" />
<workItem from="1599558681455" duration="23060000" />
<workItem from="1599633110239" duration="250000" />
<workItem from="1600079440472" duration="408000" />
<workItem from="1600096831480" duration="2472000" />
<workItem from="1600175774205" duration="25000" />
<workItem from="1600179917484" duration="407000" />
<workItem from="1600328859516" duration="262000" />
<workItem from="1602762026312" duration="11122000" />
<workItem from="1602831799597" duration="12765000" />
<workItem from="1602854134928" duration="3330000" />
<workItem from="1602857574059" duration="218000" />
<workItem from="1603107642331" duration="5950000" />
<workItem from="1603264695225" duration="33000" />
<workItem from="1603272469212" duration="17000" />
<workItem from="1603730718098" duration="118000" />
<workItem from="1603985533656" duration="12738000" />
<workItem from="1604392770203" duration="27000" />
<workItem from="1604419165739" duration="43000" />
<workItem from="1604503127063" duration="71000" />
<workItem from="1604507908338" duration="28000" />
<workItem from="1604507968294" duration="8000" />
<workItem from="1604921993804" duration="1271000" />
<workItem from="1604923532802" duration="515000" />
<workItem from="1604939815128" duration="6462000" />
<workItem from="1605006776997" duration="3000" />
<workItem from="1605201129884" duration="22000" />
<workItem from="1605201310540" duration="157000" />
<workItem from="1605599347354" duration="281000" />
<workItem from="1608113166691" duration="26421000" />
<workItem from="1608198795441" duration="30640000" />
<workItem from="1608482656314" duration="1180000" />
<workItem from="1609845987050" duration="8455000" />
<workItem from="1609856766161" duration="7420000" />
<workItem from="1609921488733" duration="11747000" />
<workItem from="1609951522474" duration="3068000" />
<workItem from="1610017442331" duration="599000" />
<workItem from="1610105883226" duration="2999000" />
<workItem from="1610111682841" duration="993000" />
<workItem from="1610113229757" duration="58000" />
<workItem from="1610114243791" duration="73000" />
<workItem from="1610708369185" duration="36000" />
<workItem from="1610709005944" duration="206000" />
<workItem from="1610709247428" duration="439000" />
<workItem from="1610718553130" duration="104000" />
<workItem from="1610729780780" duration="23000" />
<workItem from="1610990236245" duration="284000" />
<workItem from="1611071419100" duration="35000" />
<workItem from="1611138964858" duration="11000" />
</task>
<servers />
</component>
<component name="TodoView">
<todo-panel id="selected-file">
<is-autoscroll-to-source value="true" />
</todo-panel>
<todo-panel id="all">
<are-packages-shown value="true" />
<is-autoscroll-to-source value="true" />
</todo-panel>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
</project>

View file

@ -1,27 +0,0 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
},
"context7": {
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp",
"--api-key",
"ctx7sk-119cd4ab-8983-4229-8702-e84c59c34fc9"
]
},
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
{"version":1,"defects":{"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_uses_database_locking_for_invoice_numbers":5,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_prevents_double_processing_of_same_payment":8},"times":{"Tests\\Unit\\Services\\InvoiceServiceTest::it_gets_current_invoice_number":0.002,"Tests\\Unit\\Services\\InvoiceServiceTest::it_increments_invoice_number":0.002,"Tests\\Unit\\Services\\InvoiceServiceTest::it_increments_sequentially":0.004,"Tests\\Unit\\Services\\InvoiceServiceTest::it_formats_invoice_number_with_year_prefix":0.001,"Tests\\Unit\\Services\\InvoiceServiceTest::it_generates_correct_storage_paths":0.001,"Tests\\Unit\\Services\\InvoiceServiceTest::it_generates_correct_filenames":0.001,"Tests\\Unit\\Services\\InvoiceServiceTest::it_initializes_invoice_number_when_not_exists":0.002,"Tests\\Unit\\Services\\InvoiceServiceTest::it_uses_transaction_for_invoice_number_increment":0.003,"Tests\\Unit\\Services\\InvoiceServiceTest::it_locks_setting_during_increment":0.001,"Tests\\Unit\\Services\\InvoiceServiceTest::it_pads_invoice_numbers_correctly":0.001,"Tests\\Unit\\Services\\InvoiceServiceTest::it_returns_zero_when_invoice_number_not_set":0.002,"Tests\\Unit\\Services\\InvoiceServiceTest::it_handles_rapid_increments_without_gaps":0.009,"Tests\\Unit\\Services\\InvoiceServiceTest::it_returns_integer_invoice_number":0.002,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_generates_unique_invoice_numbers_under_concurrent_load":0.02,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_atomically_increments_invoice_numbers":0.004,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_does_not_skip_invoice_numbers_on_transaction_rollback":0.002,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_uses_database_locking_for_invoice_numbers":0.003,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_prevents_double_processing_of_same_payment":0.006,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_can_lock_settings_for_update":0.001,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_creates_invoice_numbers_with_correct_format":0.001,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_handles_rapid_sequential_invoice_creation":0.012,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_initializes_invoice_number_if_not_exists":0.002,"Tests\\Feature\\Payment\\ConcurrentPaymentTest::it_handles_concurrent_transaction_commits":0.003,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_locks_shopping_order_during_payment_processing":0.001,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_prevents_double_payment_processing":0.002,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_serializes_concurrent_payment_requests":0.003,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_rolls_back_payment_on_error":0.003,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_tracks_payment_status_transitions":0.003,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_handles_concurrent_payments_for_different_orders":0.003,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_isolates_payment_transactions":0.002,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_prevents_concurrent_modifications":0.001,"Tests\\Feature\\Payment\\PayoneRaceConditionTest::it_enforces_unique_payment_references":0.002}}
C:37:"PHPUnit\Runner\DefaultTestResultCache":44:{a:2:{s:7:"defects";a:0:{}s:5:"times";a:0:{}}}

448
CLAUDE.md
View file

@ -1,448 +0,0 @@
# CLAUDE.md
## Projekt-Übersicht
**mivita.care** - Laravel 11 CRM für Netzwerk-Marketing (MLM) mit E-Commerce, Provisionsberechnung, DHL-Versand und Multi-Tenancy.
## Tech Stack
| Komponente | Technologie |
| ----------- | -------------------------------------------- |
| Framework | Laravel 11 (Classic MVC) |
| Frontend | Bootstrap 4 (Appwork Theme), Blade Templates |
| Auth | Laravel Passport (OAuth2/API) |
| Database | MySQL 8 |
| Data Tables | Yajra Datatables |
| Testing | Pest PHP |
| Queue | Laravel Horizon + Redis |
| PDF | barryvdh/laravel-dompdf |
| Slugs | Eloquent-Sluggable |
| Forms | Spatie\Html |
## Wichtige Regeln (Constraints)
1. **Kein Livewire / Keine Vue-Komponenten** - Nutze reines Blade + Bootstrap
2. **Formulare** - Nutze `Spatie\Html` für Formular-Generierung (nicht reines HTML)
3. **Datentabellen** - Nutze ausschließlich `Yajra\Datatables` für Listenansichten im Admin
4. **PDF** - Rechnungen/Berichte via `barryvdh/laravel-dompdf`
5. **SEO/Slugs** - Nutze `Eloquent-Sluggable` für Berater-Profile/Produkte
6. **Code Style** - Immer `./vendor/bin/pint` nach Änderungen ausführen
## Custom Packages
| Package | Pfad | Beschreibung |
| --------------- | ------------------------------------ | --------------------------- |
| DHL Integration | `packages/acme-laravel-dhl` | Versand-Labels und Tracking |
| Shopping Cart | `packages/digital-bird/shoppingcart` | Warenkorb-System |
## Environment
This project runs inside a **Dev Container**. Commands are executed directly with `php`, `composer`, and `npm`**without** the `vendor/bin/sail` prefix. All `sail`-prefixed commands in docs or rules should be translated to their direct equivalents:
| Sail | Dev Container |
| ------------------------------ | ------------------- |
| `vendor/bin/sail artisan ...` | `php artisan ...` |
| `vendor/bin/sail composer ...` | `composer ...` |
| `vendor/bin/sail npm ...` | `npm ...` |
| `vendor/bin/sail php ...` | `php ...` |
| `vendor/bin/sail bin pint` | `./vendor/bin/pint` |
## Projekt-Struktur
```
app/
├── Console/Commands/ # Artisan Commands (Business*, User*)
├── Cron/ # Scheduled Task Logic
├── Http/Controllers/ # 40+ Controller
├── Models/ # Eloquent Models
├── Repositories/ # Data Access Layer
├── Services/ # Business Logic
│ └── BusinessPlan/ # MLM-Berechnungen
packages/ # Custom Packages
dev/ # Dokumentation
```
## Wichtige Dateien
- **MLM-Berechnung**: `app/Services/BusinessPlan/TreeCalcBotOptimized.php`
- **Cron Jobs**: `app/Console/Kernel.php`
- **Domain Resolver**: Multi-Tenant Middleware
- **Cart Systems**: AboOrderCart, HomepartyCart, ShopApiOrderCart
## Development Commands
### Laravel Basis
```bash
php artisan serve # Dev Server
php artisan migrate # Migrations
php artisan cache:clear # Cache leeren
php artisan config:clear
php artisan route:clear
php artisan view:clear
```
### Code Quality
```bash
./vendor/bin/pint # Code formatieren
./vendor/bin/pest # Tests ausführen
./vendor/bin/pest --filter=TestName # Einzelner Test
```
### IDE Helper
```bash
php artisan ide-helper:generate
php artisan ide-helper:models
php artisan ide-helper:meta
```
### Assets
```bash
npm install && npm run dev # Development
npm run production # Production Build
```
### Business Commands
```bash
# Provisionsberechnung (empfohlen: optimierte Version)
php artisan business:store-optimized {month} {year}
php artisan business:store-optimized {month} {year} --clear
# Daten löschen
php artisan business:clear-data {month} {year} [--force]
# Level Reports
php artisan business:level-reports {month} {year}
# User Management
php artisan user:cleanup
php artisan user:make_abo_order
# Zahlungen
php artisan payments:check-accounts
# Test Account
php artisan business:test-account
```
## Scheduled Tasks (Cron)
| Zeit | Command | Beschreibung |
| ----- | ------------------------------ | -------------------- |
| 02:00 | `payments:check-accounts` | Zahlungsprüfung |
| 03:00 | `business:store-optimized 0 0` | Provisionsberechnung |
| 03:30 | `user:cleanup` | User-Bereinigung |
| 04:00 | `user:make_abo_order` | Abo-Bestellungen |
## Docker Environment
```bash
docker-compose up -d # Services starten
./vendor/bin/sail artisan [command] # Artisan im Container
./vendor/bin/sail shell # Shell im Container
```
### Services
- **laravel.test** - Hauptanwendung (Traefik + SSL)
- **horizon** - Queue Worker
- **mysql** - MySQL 8.0
- **redis** - Cache/Session
- **mailpit** - Mail Testing (`mivita-mail.test`)
### Domains
- Main: `mivita.test`
- Wildcard: `*.mivita.test`
- Mail: `mivita-mail.test`
## MLM-Architektur
### Kernkonzepte
- **Consultants (Berater)**: Hierarchie mit Upline/Downline
- **TreeCalcBotOptimized**: Provisionsberechnung für MLM-Strukturen
- **BusinessPlan Services**: Sales Volumes, Ränge, Boni
### Performance
- Memory Monitoring in optimierten Commands
- Automatic Garbage Collection
- Performance Logging
## Hinweise für Claude
- Nutze immer `TreeCalcBotOptimized` für neue Business-Features
- Prüfe Custom Packages in `packages/` vor Änderungen
- Beachte Multi-Tenant Domain-Logik bei Routing-Änderungen
- DHL-Integration unterstützt Sandbox/Production Mode
===
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.12
- laravel/framework (LARAVEL) - v11
- laravel/horizon (HORIZON) - v5
- laravel/passport (PASSPORT) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/mcp (MCP) - v0
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v2
- phpunit/phpunit (PHPUNIT) - v10
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
## PHP
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== sail rules ===
## Laravel Sail
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
- Open the application in the browser by running `vendor/bin/sail open`.
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
- Install Composer packages: `vendor/bin/sail composer install`
- Execute Node commands: `vendor/bin/sail npm run dev`
- Execute PHP scripts: `vendor/bin/sail php [script]`
- View all available Sail commands by running `vendor/bin/sail` without arguments.
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test --compact` with a specific filename or filter.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `vendor/bin/sail artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
=== laravel/v11 rules ===
## Laravel 11
- Use the `search-docs` tool to get version-specific documentation.
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel 11 file structure.
- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the Laravel 11 structure unless the user explicitly requests it.
### Laravel 10 Structure
- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`.
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
- Middleware registration is in `app/Http/Kernel.php`
- Exception handling is in `app/Exceptions/Handler.php`
- Console commands and schedule registration is in `app/Console/Kernel.php`
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
### New Artisan Commands
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
- `vendor/bin/sail artisan make:enum`
- `vendor/bin/sail artisan make:class`
- `vendor/bin/sail artisan make:interface`
=== pint/core rules ===
## Laravel Pint Code Formatter
- You must run `vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/sail bin pint --test --format agent`, simply run `vendor/bin/sail bin pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
### Testing
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
- All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
- Pest tests look and behave like this:
<code-snippet name="Basic Pest Test Example" lang="php">
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
### Running Tests
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
- To run all tests: `vendor/bin/sail artisan test --compact`.
- To run all tests in a file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `vendor/bin/sail artisan test --compact --filter=testName` (recommended after making a change to a related file).
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
### Pest Assertions
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
it('returns all', function () {
$response = $this->postJson('/api/docs', []);
$response->assertSuccessful();
});
</code-snippet>
### Mocking
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
- Use datasets in Pest to simplify tests that have a lot of duplicated data. This is often the case when testing validation rules, so consider this solution when writing tests for validation rules.
<code-snippet name="Pest Dataset Example" lang="php">
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
</laravel-boost-guidelines>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
<p class="font-lato weight-300 size-20 nomargin-bottom">
Du möchtest Vertriebspartner werden oder hast Fragen zu unseren Produkten?
</p>
<h3>Jetzt Kontakt aufnehmen: <span><strong style="color:#566d56">+49 (0) 8333 94 61 767</strong></span></h3>
<h3>Jetzt Kontakt aufnehmen: <span><strong style="color:#566d56">+49 (0) 8333 946 98 90</strong></span></h3>
</div><!-- /left text -->
@ -41,15 +41,15 @@
<address>
<ul class="list-unstyled">
<li class="footer-sprite address">
mivita care GmbH<br>
mivita e.K.<br>
Leinfeld 2<br>
87755 Kirchhaslach<br>
</li>
<li class="footer-sprite phone">
Telefon: +49 (0) 8333 94 61 767
Telefon: +49 (0) 8333 946 98 90
</li>
<li class="footer-sprite email">
<a href="mailto:info@mivita.care">info@mivita.care</a>
<a href="mailto:info@riwa-tec.de">info@mivita.care</a>
</li>
</ul>
</address>

View file

@ -46,11 +46,12 @@ Zur besseren Verständlichkeit unserer Datenschutzerklärung möchten wir Ihnen
<br>
Verantwortlicher im Sinne der Datenschutz-Grundverordnung sowie der in den Mitgliedstaaten der Europäischen Union geltenden Datenschutzgesetze und anderer datenschutzrechtlicher Bestimmungen ist:
<br><br>
<strong>mivita care GmbH</strong><br>
<strong>mivita e.K.</strong><br>
Herr Alois Ried<br>
Leinfeld 2<br>
87755 Kirchhaslach<br>
Telefon: +49 (0) 8333 94 61 767<br>
Telefon: +49 (0) 8333 946 98 90<br>
Fax: +49 (0) 8333 7268<br>
Mail: info@mivita.care<br>
<br><br>
<strong>III. Cookies</strong>

View file

@ -8,23 +8,23 @@
<h3 class="box-title m-b-0">Impressum</h3>
<hr>
<p><strong>mivita care GmbH</strong><br>
<p><strong>mivita e.K.</strong><br>
Leinfeld 2<br>
87755 Kirchhaslach<br>
Telefon: +49 (0) 8333 94 61 767<br>
Telefon: +49 (0) 8333 946 98 90<br>
Fax: +49 (0) 8333 7268<br>
E-Mail: info@mivita.care<br></p>
<p><strong>Geschäftsführer:</strong><br> Alois Ried<br><br>
<p><strong>Geschäftsinhaber:</strong><br> Alois Ried<br><br>
<strong>Registergericht:</strong><br> Memmingen<br><br>
<strong>Registernummer:</strong><br> HRB 21591<br><br>
<strong>Registernummer:</strong><br> HRA 12236<br><br>
<strong>USt-ID-Nr.:</strong><br> DE 453867883<br></p>
<strong>USt-ID-Nr.:</strong><br> DE 244162340<br></p>
<br>
<p><strong>Support mivita:</strong><br>
Telefon: +49 (0) 8333 94 61 767<br>
Telefon: +49 (0) 8333 946 98 90<br>
E-Mail:: <a href="mailto:info@mivita.care">info@mivita.care</a></p>
<br>

View file

@ -141,13 +141,13 @@
<hr/>
<p>
<span class="block"><strong><i class="fa fa-map-marker"></i> Adresse:<br></strong> mivita care GmbH<br>
<span class="block"><strong><i class="fa fa-map-marker"></i> Adresse:<br></strong> mivita e.K.<br>
Leinfeld 2<br>
87755 Kirchhaslach</span>
<span class="block"><strong><i class="fa fa-phone"></i> Telefon:</strong> <a
href="tel:0 8333-94 61 767">0 8333-94 61 767</a></span>
href="tel:0 8333-946 98 90">0 8333-946 98 90</a></span>
<span class="block"><strong><i class="fa fa-envelope"></i> Email:</strong> <a
href="mailto:info@mivita.care">info@mivita.care</a></span>
href="mailto:info@riwa-tec.de">info@mivita.care</a></span>
</p>
<hr/>

View file

@ -1,4 +1,2 @@
TODOS
- member_id bei Abos aktualisieren, wenn User neu zugewiesen wird oder gelöscht wird
-

View file

@ -1,169 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\AboChartSnapshot;
use App\Models\UserAbo;
use App\Services\AboHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
class AboStoreChartSnapshots extends Command
{
protected $signature = 'abo:store-chart-snapshots
{--user= : Nur einen bestimmten User berechnen (user_id)}
{--force : Bereits vorhandene Snapshots überschreiben}';
protected $description = 'Speichert monatliche Abo-Zählungen aller vergangenen Monate in der Datenbank (einmalig je Monat)';
private const SCOPES = ['ot', 'team_abos', 'team_cust_abos'];
private const START_YEAR = 2026;
public function handle(): int
{
$now = Carbon::now();
$force = (bool) $this->option('force');
// Monate die eingefroren werden sollen: von START_YEAR/01 bis letzten Monat
$months = $this->getPastMonths($now);
if (empty($months)) {
$this->info('Keine vergangenen Monate zum Speichern.');
return self::SUCCESS;
}
// User ermitteln
$userQuery = User::whereNotNull('m_level')
->whereNotNull('payment_account')
->where('admin', '<', 4)
->whereNull('deleted_at');
if ($userId = $this->option('user')) {
$userQuery->where('id', $userId);
}
$users = $userQuery->select('id')->get();
$total = $users->count();
$this->info("Berechne Snapshots für {$total} User, ".count($months).' Monate, '.count(self::SCOPES).' Scopes...');
$bar = $this->output->createProgressBar($total);
$bar->start();
$inserted = 0;
$skipped = 0;
foreach ($users as $user) {
// Bereits vorhandene Snapshots für diesen User laden (zum Überspringen)
$existing = AboChartSnapshot::where('user_id', $user->id)
->get()
->keyBy(fn ($s) => "{$s->scope}_{$s->year}_{$s->month}");
$teamUserIds = AboHelper::getTeamUserIds($user->id);
$rows = [];
foreach ($months as [$year, $month]) {
$startOfMonth = Carbon::create($year, $month, 1)->startOfMonth();
$endOfMonth = Carbon::create($year, $month, 1)->endOfMonth();
foreach (self::SCOPES as $scope) {
$key = "{$scope}_{$year}_{$month}";
if (! $force && $existing->has($key)) {
$skipped++;
continue;
}
$count = $this->calculateCount($scope, $user->id, $teamUserIds, $startOfMonth, $endOfMonth);
$rows[] = [
'user_id' => $user->id,
'scope' => $scope,
'year' => $year,
'month' => $month,
'count' => $count,
'calculated_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
$inserted++;
}
}
if (! empty($rows)) {
if ($force) {
foreach ($rows as $row) {
AboChartSnapshot::updateOrInsert(
['user_id' => $row['user_id'], 'scope' => $row['scope'], 'year' => $row['year'], 'month' => $row['month']],
$row
);
}
} else {
AboChartSnapshot::insertOrIgnore($rows);
}
}
$bar->advance();
gc_collect_cycles();
}
$bar->finish();
$this->newLine();
$this->info("Fertig. Gespeichert: {$inserted}, Übersprungen (bereits vorhanden): {$skipped}");
return self::SUCCESS;
}
/**
* Berechnet die Abo-Anzahl für einen Scope/User/Monat anhand der tatsächlichen Daten zum Zeitpunkt der Berechnung.
*
* @param int[] $teamUserIds
*/
private function calculateCount(string $scope, int $userId, array $teamUserIds, Carbon $startOfMonth, Carbon $endOfMonth): int
{
$terminalStatuses = [4, 5];
$query = match ($scope) {
'ot' => UserAbo::where('member_id', $userId)
->where('is_for', 'ot')
->where('status', '>', 1),
'team_abos' => UserAbo::whereIn('user_id', $teamUserIds)
->where('is_for', 'me')
->where('status', '>', 1),
'team_cust_abos' => UserAbo::whereIn('member_id', $teamUserIds)
->where('is_for', 'ot')
->where('status', '>', 1),
};
return $query
->whereDate('start_date', '<=', $endOfMonth)
->where(function ($q) use ($startOfMonth, $terminalStatuses) {
$q->whereDate('cancel_date', '>=', $startOfMonth)
->orWhere(function ($q2) use ($terminalStatuses) {
$q2->whereNull('cancel_date')
->whereNotIn('status', $terminalStatuses);
});
})
->count();
}
/**
* Alle abgeschlossenen Monate von START_YEAR/01 bis letzten Monat.
*
* @return array<array{int, int}>
*/
private function getPastMonths(Carbon $now): array
{
$months = [];
$cursor = Carbon::create(self::START_YEAR, 1, 1);
$lastMonth = $now->copy()->subMonth()->endOfMonth();
while ($cursor->lte($lastMonth)) {
$months[] = [(int) $cursor->year, (int) $cursor->month];
$cursor->addMonth();
}
return $months;
}
}

View file

@ -1,136 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\UserBusinessStructure;
use App\Models\UserBusiness;
use Illuminate\Console\Command;
class BusinessClearData extends Command
{
/**
* php artisan business:clear-data {month} {year}
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'business:clear-data {month} {year} {--force : Force deletion without confirmation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear stored business structure data for a specific month/year';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$month = (int) $this->argument('month');
$year = (int) $this->argument('year');
// Validierung
if ($month < 1 || $month > 12) {
$this->error('Invalid month. Must be between 1 and 12.');
return 1;
}
$currentYear = (int) date('Y');
if ($year < 2020 || $year > $currentYear + 1) {
$this->error('Invalid year. Must be between 2020 and ' . ($currentYear + 1));
return 1;
}
$this->info("Preparing to clear business data for month: {$month} | year: {$year}");
// Finde bestehende Struktur
$existingStructure = UserBusinessStructure::where('year', $year)
->where('month', $month)
->first();
if (!$existingStructure) {
$this->info('No stored business structure found for the specified month/year');
return 0;
}
$structureId = $existingStructure->id;
$userBusinessCount = UserBusiness::where('b_structure_id', $structureId)->count();
$userCount = is_array($existingStructure->users) ? count($existingStructure->users) : 0;
$this->info("Found structure ID: {$structureId}");
$this->info("- UserBusiness records: {$userBusinessCount}");
$this->info("- Users in structure: {$userCount}");
$this->info("- Completed: " . ($existingStructure->completed ? 'Yes' : 'No'));
// Bestätigung (außer bei --force)
if (!$this->option('force')) {
if (!$this->confirm('Are you sure you want to delete this business structure data?')) {
$this->info('Operation cancelled by user');
return 0;
}
}
$startTime = microtime(true);
// Lösche UserBusiness Einträge
if ($userBusinessCount > 0) {
$this->info("Deleting {$userBusinessCount} UserBusiness records...");
UserBusiness::where('b_structure_id', $structureId)->delete();
$this->info('✓ UserBusiness records deleted');
}
// Lösche UserBusinessStructure
$this->info('Deleting UserBusinessStructure...');
$existingStructure->delete();
$this->info('✓ UserBusinessStructure deleted');
// Garbage Collection
gc_collect_cycles();
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$this->info("✅ Successfully cleared all business data in {$duration}ms");
$this->logMemoryUsage();
return 0;
} catch (\Exception $e) {
$this->error('Error clearing business data: ' . $e->getMessage());
$this->error('Stack trace: ' . $e->getTraceAsString());
return 1;
}
}
/**
* Loggt aktuelle Memory-Nutzung
*/
private function logMemoryUsage(): void
{
$currentMemory = memory_get_usage();
$peakMemory = memory_get_peak_usage();
$currentFormatted = $this->formatBytes($currentMemory);
$peakFormatted = $this->formatBytes($peakMemory);
$this->info("Memory - Current: {$currentFormatted} | Peak: {$peakFormatted}");
}
/**
* Formatiert Bytes in lesbare Einheiten
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = array('B', 'KB', 'MB', 'GB', 'TB');
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}

View file

@ -1,149 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\LevelReportService;
use Illuminate\Console\Command;
class BusinessLevelReports extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'business:level-reports {--month= : Filter by specific month} {--year= : Filter by specific year} {--user-id= : Filter by specific user ID} {--csv : Export as CSV file} {--not-updated : Show only users not yet updated to their new level}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate reports showing who achieved new career levels and when';
private $levelReportService;
public function __construct(LevelReportService $levelReportService)
{
parent::__construct();
$this->levelReportService = $levelReportService;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$this->info('Generiere Level-Aufstieg-Report...');
// Filter Parameter
$filters = [
'month' => $this->option('month'),
'year' => $this->option('year'),
'user_id' => $this->option('user-id'),
'only_not_updated' => $this->option('not-updated')
];
$exportCsv = $this->option('csv');
// Lade Level-Aufstiege über Service
$levelPromotions = $this->levelReportService->getLevelPromotions($filters);
if ($levelPromotions->isEmpty()) {
$this->info('Keine Level-Aufstiege gefunden.');
return 0;
}
if ($exportCsv) {
$filepath = $this->levelReportService->exportToCsv($levelPromotions);
$this->info('');
$this->info('CSV-Export erstellt: ' . $filepath);
$this->info('Anzahl Datensätze: ' . $levelPromotions->count());
} else {
$this->displayReport($levelPromotions);
}
$this->info('Report erfolgreich generiert.');
return 0;
} catch (\Exception $e) {
$this->error('Fehler beim Generieren des Reports: ' . $e->getMessage());
return 1;
}
}
private function displayReport($promotions)
{
$statistics = $this->levelReportService->getStatistics($promotions);
$this->info('');
$this->info('=== LEVEL-AUFSTIEG REPORT ===');
$this->info('');
if ($promotions->isEmpty()) {
$this->info('Keine Level-Aufstiege gefunden.');
return;
}
$headers = [
'Datum',
'User ID',
'Name',
'E-Mail',
'Von Level',
'Zu Level',
'Aktueller Level',
'Margin',
'KP Req',
'PP Req',
'Growth Bonus',
'User PP',
'User KP',
'Level Update',
'Aktiv'
];
$rows = [];
foreach ($promotions->toArray() as $promotion) {
$rows[] = [
$promotion['date'],
$promotion['user_id'],
$promotion['first_name'] . ' ' . $promotion['last_name'],
$promotion['email'],
$promotion['from_level_name'] . ' (ID:' . $promotion['from_level_id'] . ')',
$promotion['to_level_name'] . ' (ID:' . $promotion['to_level_id'] . ')',
$promotion['current_user_level_name'] . ' (ID:' . ($promotion['current_user_level_id'] ?? 'N/A') . ')',
$promotion['to_level_margin'] . '%',
number_format($promotion['to_level_qual_kp'], 0, ',', '.'),
number_format($promotion['to_level_qual_pp'], 0, ',', '.'),
$promotion['to_level_growth_bonus'] . '%',
number_format($promotion['total_pp'], 0, ',', '.'),
number_format($promotion['sales_volume_points_sum'], 0, ',', '.'),
$promotion['level_updated'],
$promotion['active_account'],
];
}
$this->table($headers, $rows);
// Zusammenfassung
$this->info('');
$this->info('=== ZUSAMMENFASSUNG ===');
$this->info('Anzahl Level-Aufstiege: ' . $statistics['total_count']);
$this->info('');
$this->info('Aufstiege nach Ziel-Level:');
foreach ($statistics['level_stats'] as $level => $count) {
$this->info(" - {$level}: {$count}");
}
$this->info('');
$this->info('Aufstiege nach Zeitraum:');
foreach ($statistics['period_stats'] as $period => $count) {
$this->info(" - {$period}: {$count}");
}
}
}

View file

@ -2,11 +2,11 @@
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
use App\Cron\BusinessUsersStore;
use App\Cron\UserLevelUpdate;
use App\Cron\UserPaymentCredits;
use App\Models\Setting;
use Illuminate\Console\Command;
class BusinessStore extends Command
{
@ -17,24 +17,23 @@ class BusinessStore extends Command
*
* @var string
*/
protected $signature = 'business:store {month} {year}';
protected $signature = 'business:store {month} {year}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create Business Structure and UserDetails with optimized performance';
protected $description = 'Create Business Structur and UserDetails';
private $timeStart;
private $month;
private $year;
private $sendCreditMail = false;
private $sendUpdateMail = true;
private $sendUpdateMail = false;
/**
* Create a new command instance.
@ -46,58 +45,6 @@ class BusinessStore extends Command
parent::__construct();
}
/**
* Prüft ob der Command heute ausgeführt werden soll
*
* WICHTIG: Diese Methode verhindert, dass der Command täglich läuft!
* Der Command sollte nur am konfigurierten Tag des Monats laufen.
*
* @return bool True wenn Command ausgeführt werden soll, False sonst
*/
private function shouldExecuteToday(): bool
{
// Hole konfigurierten Ausführungstag (Standard: 1 = Monatserster)
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
// Fallback: Wenn Setting leer oder 0, verwende Tag 1
if ($executeDay === 0) {
$executeDay = 1;
$this->warn('Setting "day-exectute-business-structur" ist leer oder 0. Verwende Standard: Tag 1');
\Log::channel('cron')->warning('BusinessStore: Setting day-exectute-business-structur is empty, using default: 1');
}
$presentDay = (int) date('d');
// Logging für Debugging
$this->info("BusinessStore: Configured Day: {$executeDay}, Present Day: {$presentDay}");
\Log::channel('cron')->info("BusinessStore: Configured Day: {$executeDay}, Present Day: {$presentDay}");
// Prüfe ob heute der konfigurierte Tag ist
if ($executeDay !== $presentDay) {
// Erlaubnis zum Überschreiben für Entwicklung/Testing
// ENV-Variable BUSINESS_FORCE_EXECUTE=true überschreibt den Check
if (env('BUSINESS_FORCE_EXECUTE', false) === true) {
$this->warn('⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!');
$this->warn('⚠️ Dies sollte NUR auf Test-Servern verwendet werden!');
\Log::channel('cron')->warning('BusinessStore: FORCED execution via BUSINESS_FORCE_EXECUTE');
return true;
}
// Command sollte heute NICHT laufen
$this->info("❌ Command wird NICHT ausgeführt - falscher Tag (erwartet: {$executeDay}, heute: {$presentDay})");
\Log::channel('cron')->info("BusinessStore: NOT EXECUTED - wrong day (expected: {$executeDay}, today: {$presentDay})");
return false;
}
// Command wird ausgeführt
$this->info("✅ Command wird ausgeführt - korrekter Tag ({$presentDay})");
\Log::channel('cron')->info("BusinessStore: EXECUTING - correct day ({$presentDay})");
return true;
}
/**
* Execute the console command.
*
@ -105,131 +52,139 @@ class BusinessStore extends Command
*/
public function handle()
{
try {
// Prüfe ob Command am richtigen Tag ausgeführt werden soll
if (! $this->shouldExecuteToday()) {
return 0;
}
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
$this->info('RUN Command BusinessStore on Day: '. $executeDay);
$this->logMemoryUsage('Command Start');
$this->timeStart = microtime(true);
// Argumente mit Standardwerten für den Vormonat
$this->month = $this->argument('month') ?: (int) date('m', strtotime('-1 month'));
$this->year = $this->argument('year') ?: (int) date('Y', strtotime('-1 month'));
$this->info('RUN Command BusinessStore on month: '.$this->month.' | year: '.$this->year);
\Log::channel('cron')->info('RUN Command BusinessStore on month: '.$this->month.' | year: '.$this->year);
$this->logMemoryUsage('Parameters initialized');
// Prozesse ausführen mit Fehlerbehandlung
$this->executeWithErrorHandling('Business Structure Storage', function () {
$this->storeBusinessStructureUsersDetailMonth();
});
$this->executeWithErrorHandling('Commission Calculation', function () {
$this->userBusinessCommissionsToCredit();
});
// Auskommentierte Prozesse bleiben inaktiv
// $this->userCreatePaymentCreditsPDF();
// $this->userLevelUpdate();
// $this->storeBusinessStructureUsersDetailPeriod(1, 6);
$this->logExecutionTime('COMMAND COMPLETED SUCCESSFULLY');
$this->logMemoryUsage('Command End');
\Log::channel('cron')->info('COMMAND COMPLETED SUCCESSFULLY');
$presentDay = (int) date('d');
$this->info('RUN Command BusinessStore present Day: '. $presentDay);
if($executeDay !== $presentDay){
$this->info('NOT RUN Command BusinessStore is not present Day: '. $presentDay);
return 0;
} catch (\Exception $e) {
$this->error('Command failed with error: '.$e->getMessage());
$this->error('Stack trace: '.$e->getTraceAsString());
$this->logExecutionTime('COMMAND FAILED');
return 1;
}
$this->timeStart = microtime(true);
//$this->info('RUN Command BusinessStore');
$this->month = $this->argument('month');
$this->year = $this->argument('year');
if(!$this->month || !$this->year){
$this->month = (int) date("m", strtotime("-1 month", time()));
$this->year = (int) date("Y", strtotime("-1 month", time()));
}
$this->info('RUN Command BusinessStore on month: '.$this->month.' | year: '.$this->year);
//erstellt die Business Struktur und die Details
$this->storeBusinessStructureUsersDetailMonth();
//Struktur und die Details für mehrere Montate for / to month
//$this->storeBusinessStructureUsersDetailPeriod(1, 6);
//erstellt die Gutschriften
//$this->userBusinessCommissionsToCredit();
//erstellt aus den Gutschriften die PDFs
//$this->userCreatePaymentCreditsPDF();
//update user Level
//$this->userLevelUpdate();
return 0;
//\Log::info('Cron is running');
//return 0;
}
private function storeBusinessStructureUsersDetailMonth()
{
private function storeBusinessStructureUsersDetailMonth(){
$this->info('storeBusinessStructureUsersDetailMonth month: '.$this->month.' year:'.$this->year);
$businessUsersStore = new BusinessUsersStore($this->month, $this->year);
$businessUsersStore->storeUserBusinessStructure();
$businessUsersStore->storeBusinessUsersDetail();
$bool = $businessUsersStore->storeBusinessCompleted();
$this->logExecutionTime('END Command storeBusinessStructureUsersDetailMonth: '.$bool);
$businessUsersStore = new BusinessUsersStore($this->month, $this->year);
$businessUsersStore->storeUserBusinessStructure();
$businessUsersStore->storeBusinessUsersDetail();
$bool = $businessUsersStore->storeBusinessCompleted();
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command storeBusinessStructureUsersDetailMonth: '.$bool. ' | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
private function userBusinessCommissionsToCredit()
{
private function userBusinessCommissionsToCredit(){
$this->info('userBusinessCommissionsToCredit month: '.$this->month.' year:'.$this->year);
$userPaymentCredits = new UserPaymentCredits($this->month, $this->year);
$userBusinesses = $userPaymentCredits->getUserBusinessByMonthYear();
foreach ($userBusinesses as $userBusiness) {
foreach($userBusinesses as $userBusiness){
$ret = $userPaymentCredits->addUserCreditItem($userBusiness);
$this->info('userBusinessCredit: '.$ret->user_id.' : Team: '.$ret->commission_pp_total.' | Shop: '.$ret->commission_shop_sales);
$this->info('userBusinessCredit: '.$ret->user_id.' : Team: '.$ret->commission_team_total.' | Shop: '.$ret->commission_shop_sales);
}
$this->logExecutionTime('END Command userBusinessCommissionsToCredit:');
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command userBusinessCommissionsToCredit: | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
private function userCreatePaymentCreditsPDF()
{
private function userCreatePaymentCreditsPDF(){
$this->info('userCreatePaymentCreditsPDF month: '.$this->month.' year:'.$this->year);
$userPaymentCredits = new UserPaymentCredits($this->month, $this->year);
$creditItemUsers = $userPaymentCredits->getUserCreditItemUsersByMonthYear();
foreach ($creditItemUsers as $creditItemUser) {
foreach($creditItemUsers as $creditItemUser){
$bool = $userPaymentCredits->makeCreditPaymentPDF($creditItemUser->user_id, $this->sendCreditMail);
$this->info('creditsPDF: '.$bool.' user_id: '.$creditItemUser->user_id);
}
$this->logExecutionTime('END Command userCreatePaymentCreditsPDF:');
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command userCreatePaymentCreditsPDF: | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
private function userLevelUpdate()
{
private function userLevelUpdate(){
$this->info('userLevelUpdate month: '.$this->month.' year:'.$this->year);
$userLevelUpdate = new UserLevelUpdate($this->month, $this->year);
$levelUpdateUsers = $userLevelUpdate->getUserBusinessByMonthYear();
foreach ($levelUpdateUsers as $userBusiness) {
foreach($levelUpdateUsers as $userBusiness){
$ret = $userLevelUpdate->makeUserLevelUpdate($userBusiness, $this->sendUpdateMail);
if ($ret) {
if($ret){
$this->info('updateLevel: '.$userBusiness->user->id.' | '.$userBusiness->user->email.' | '.
'from: '.$userBusiness->m_level_id.' '.$userBusiness->user_level_name.' | '.
'to: '.$ret);
'from: '.$userBusiness->m_level_id.' '.$userBusiness->user_level_name.' | '.
'to: '.$ret);
}
}
$this->logExecutionTime('END Command userLevelUpdate:');
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command userLevelUpdate: | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
private function storeBusinessStructureUsersDetailPeriod($from, $to)
{
for ($i = $from; $i <= $to; $i++) {
$this->info('Store Business Structure Users Detail month: '.$i.' year:'.$this->year);
$businessUsersStore = new BusinessUsersStore($i, $this->year);
private function storeBusinessStructureUsersDetailPeriod($for, $to){
for($i = $for; $i<=$to; $i++){
$month = $i;
$this->info('Store Business Structure Users Detail month: '.$month.' year:'.$this->year);
$businessUsersStore = new BusinessUsersStore($month, $this->year);
$businessUsersStore->storeUserBusinessStructure();
$businessUsersStore->storeBusinessUsersDetail();
$bool = $businessUsersStore->storeBusinessCompleted();
$this->logExecutionTime('Period BusinessStore: '.$bool);
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('Period BusinessStore: '.$bool. ' | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
}
}
private function logExecutionTime($message)
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info($message.' | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms');
}
}

View file

@ -1,526 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Cron\BusinessUsersStoreOptimized;
use App\Cron\UserLevelUpdate;
use App\Cron\UserPaymentCredits;
use App\Models\Setting;
use App\Models\UserBusiness;
use App\Models\UserBusinessStructure;
use App\Models\UserSalesVolume;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\User;
use Illuminate\Console\Command;
class BusinessStoreOptimized extends Command
{
/**
* ln -sfv /usr/bin/php73 /usr/bin/php
* php artisan business:store-optimized month year
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'business:store-optimized {month} {year} {--clear : Clear stored data before processing}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create Business Structure and UserDetails with optimized performance and monitoring';
private $timeStart;
private $month;
private $year;
private $sendCreditMail = false;
private $sendUpdateMail = false;
/**
* Getter für sendUpdateMail (für Tests)
*/
public function getSendUpdateMail(): bool
{
return $this->sendUpdateMail;
}
/**
* Setter für sendUpdateMail (für Tests)
*/
public function setSendUpdateMail(bool $sendUpdateMail): void
{
$this->sendUpdateMail = $sendUpdateMail;
}
/**
* Prüft ob der Command heute ausgeführt werden soll
*
* WICHTIG: Diese Methode verhindert, dass der Command täglich läuft!
* Der Command sollte nur am konfigurierten Tag des Monats laufen.
*
* @return bool True wenn Command ausgeführt werden soll, False sonst
*/
private function shouldExecuteToday(): bool
{
// Hole konfigurierten Ausführungstag (Standard: 1 = Monatserster)
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
// Fallback: Wenn Setting leer oder 0, verwende Tag 1
if ($executeDay === 0) {
$executeDay = 1;
$this->warn('Setting "day-exectute-business-structur" ist leer oder 0. Verwende Standard: Tag 1');
\Log::channel('cron')->warning('BusinessStoreOptimized: Setting day-exectute-business-structur is empty, using default: 1');
}
$presentDay = (int) date('d');
// Logging für Debugging
$this->info("BusinessStoreOptimized: Configured Day: {$executeDay}, Present Day: {$presentDay}");
\Log::channel('cron')->info("BusinessStoreOptimized: Configured Day: {$executeDay}, Present Day: {$presentDay}");
// Prüfe ob heute der konfigurierte Tag ist
if ($executeDay !== $presentDay) {
// Erlaubnis zum Überschreiben für Entwicklung/Testing
// ENV-Variable BUSINESS_FORCE_EXECUTE=true überschreibt den Check
if (env('BUSINESS_FORCE_EXECUTE', false) === true) {
$this->warn('⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!');
$this->warn('⚠️ Dies sollte NUR auf Test-Servern verwendet werden!');
\Log::channel('cron')->warning('BusinessStoreOptimized: FORCED execution via BUSINESS_FORCE_EXECUTE');
return true;
}
// Command sollte heute NICHT laufen
$this->info("❌ Command wird NICHT ausgeführt - falscher Tag (erwartet: {$executeDay}, heute: {$presentDay})");
\Log::channel('cron')->info("BusinessStoreOptimized: NOT EXECUTED - wrong day (expected: {$executeDay}, today: {$presentDay})");
return false;
}
// Command wird ausgeführt
$this->info("✅ Command wird ausgeführt - korrekter Tag ({$presentDay})");
\Log::channel('cron')->info("BusinessStoreOptimized: EXECUTING - correct day ({$presentDay})");
return true;
}
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
// Prüfe ob Command am richtigen Tag ausgeführt werden soll
if (! $this->shouldExecuteToday()) {
return 0;
}
$this->logMemoryUsage('Command Start');
$this->timeStart = microtime(true);
// Argumente mit Standardwerten für den Vormonat
$this->month = $this->argument('month') ?: (int) date('m', strtotime('-1 month'));
$this->year = $this->argument('year') ?: (int) date('Y', strtotime('-1 month'));
$this->info('RUN Command BusinessStoreOptimized on month: '.$this->month.' | year: '.$this->year);
$this->logMemoryUsage('Parameters initialized');
// Prüfe --clear Option und lösche gespeicherte Daten falls gewünscht
if ($this->option('clear')) {
$this->executeWithErrorHandling('Clear Stored Data', function () {
$this->clearStoredData();
});
}
// Prozesse ausführen mit optimierter Fehlerbehandlung
$this->executeWithErrorHandling('Business Structure Storage', function () {
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized Business Structure Storage');
$this->storeBusinessStructureUsersDetailMonth();
});
$this->executeWithErrorHandling('Commission Calculation', function () {
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized Commission Calculation');
$this->userBusinessCommissionsToCredit();
});
$this->executeWithErrorHandling('User Level Update', function () {
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized User Level Update');
$this->userLevelUpdate();
});
$this->executeWithErrorHandling('Monthly Qual-KP Bonus Points', function () {
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized Monthly Qual-KP Bonus Points');
$this->assignMonthlyQualKpBonusPoints();
});
// Auskommentierte Prozesse bleiben inaktiv
// $this->userCreatePaymentCreditsPDF();
// $this->storeBusinessStructureUsersDetailPeriod(1, 6);
$this->logExecutionTime('COMMAND COMPLETED SUCCESSFULLY');
$this->logMemoryUsage('Command End');
\Log::channel('cron')->info('COMMAND COMPLETED SUCCESSFULLY');
return 0;
} catch (\Exception $e) {
$this->error('Command failed with error: '.$e->getMessage());
$this->error('Stack trace: '.$e->getTraceAsString());
$this->logExecutionTime('COMMAND FAILED');
\Log::channel('cron')->info('COMMAND FAILED');
return 1;
}
}
private function storeBusinessStructureUsersDetailMonth()
{
$this->info('storeBusinessStructureUsersDetailMonth month: '.$this->month.' year:'.$this->year);
try {
$businessUsersStore = new BusinessUsersStoreOptimized($this->month, $this->year);
$businessUsersStore->storeUserBusinessStructure();
$businessUsersStore->storeBusinessUsersDetail();
$bool = $businessUsersStore->storeBusinessCompleted();
$this->logExecutionTime('END Command storeBusinessStructureUsersDetailMonth: '.$bool);
} catch (\Exception $e) {
$this->error('Error in storeBusinessStructureUsersDetailMonth: '.$e->getMessage());
throw $e;
}
}
private function userBusinessCommissionsToCredit()
{
$this->info('userBusinessCommissionsToCredit month: '.$this->month.' year:'.$this->year);
try {
$userPaymentCredits = new UserPaymentCredits($this->month, $this->year);
$userBusinesses = $userPaymentCredits->getUserBusinessByMonthYear();
$processedCount = 0;
foreach ($userBusinesses as $userBusiness) {
$ret = $userPaymentCredits->addUserCreditItem($userBusiness);
$this->info('userBusinessCredit: '.$ret->user_id.' : Team: '.$ret->commission_pp_total.' | Shop: '.$ret->commission_shop_sales);
$processedCount++;
// Memory-Check alle 100 User
if ($processedCount % 100 === 0) {
$this->logMemoryUsage("After processing {$processedCount} users");
}
}
$this->info("Processed {$processedCount} user businesses total");
$this->logExecutionTime('END Command userBusinessCommissionsToCredit:');
} catch (\Exception $e) {
$this->error('Error in userBusinessCommissionsToCredit: '.$e->getMessage());
throw $e;
}
}
private function userCreatePaymentCreditsPDF()
{
$this->info('userCreatePaymentCreditsPDF month: '.$this->month.' year:'.$this->year);
try {
$userPaymentCredits = new UserPaymentCredits($this->month, $this->year);
$creditItemUsers = $userPaymentCredits->getUserCreditItemUsersByMonthYear();
$processedCount = 0;
foreach ($creditItemUsers as $creditItemUser) {
$bool = $userPaymentCredits->makeCreditPaymentPDF($creditItemUser->user_id, $this->sendCreditMail);
$this->info('creditsPDF: '.$bool.' user_id: '.$creditItemUser->user_id);
$processedCount++;
// Memory-Check alle 50 PDFs
if ($processedCount % 50 === 0) {
$this->logMemoryUsage("After processing {$processedCount} PDFs");
}
}
$this->info("Created {$processedCount} PDF files total");
$this->logExecutionTime('END Command userCreatePaymentCreditsPDF:');
} catch (\Exception $e) {
$this->error('Error in userCreatePaymentCreditsPDF: '.$e->getMessage());
throw $e;
}
}
/**
* Aktualisiert User-Level basierend auf next_qual_user_level
* Kann auch von Tests aufgerufen werden
*/
public function userLevelUpdate()
{
$this->info('userLevelUpdate month: '.$this->month.' year:'.$this->year);
try {
$userLevelUpdate = new UserLevelUpdate($this->month, $this->year);
$levelUpdateUsers = $userLevelUpdate->getUserBusinessByMonthYear();
$this->info('Found '.$levelUpdateUsers->count().' user businesses with level promotions to process');
$updatedCount = 0;
$skippedCount = 0;
$errorCount = 0;
foreach ($levelUpdateUsers as $userBusiness) {
try {
$ret = $userLevelUpdate->makeUserLevelUpdate($userBusiness, $this->sendUpdateMail);
if ($ret) {
$oldLevel = $userBusiness->m_level_id.' '.($userBusiness->user_level_name ?? 'N/A');
$this->info('updateLevel: User '.$userBusiness->user->id.
' | '.$userBusiness->user->email.
' | from: '.$oldLevel.
' | to: '.$ret);
$updatedCount++;
} else {
$skippedCount++;
}
// Memory-Check alle 50 User
if (($updatedCount + $skippedCount) % 50 === 0) {
$this->logMemoryUsage('After processing '.($updatedCount + $skippedCount).' users');
}
} catch (\Exception $e) {
$errorCount++;
$this->warn('Error updating level for UserBusiness '.$userBusiness->id.': '.$e->getMessage());
\Log::channel('cron')->warning('UserLevelUpdate error for UserBusiness '.$userBusiness->id.': '.$e->getMessage());
// Weiter mit nächstem User statt abzubrechen
continue;
}
}
$this->info("Level update completed: {$updatedCount} updated, {$skippedCount} skipped, {$errorCount} errors");
$this->logExecutionTime('END Command userLevelUpdate:');
$this->logMemoryUsage('After userLevelUpdate');
} catch (\Exception $e) {
$this->error('Error in userLevelUpdate: '.$e->getMessage());
$this->error('Stack trace: '.$e->getTraceAsString());
\Log::channel('cron')->error('UserLevelUpdate command failed: '.$e->getMessage());
throw $e;
}
}
private function storeBusinessStructureUsersDetailPeriod($from, $to)
{
try {
for ($i = $from; $i <= $to; $i++) {
$this->info('Store Business Structure Users Detail month: '.$i.' year:'.$this->year);
$this->logMemoryUsage("Before month {$i}");
$businessUsersStore = new BusinessUsersStoreOptimized($i, $this->year);
$businessUsersStore->storeUserBusinessStructure();
$businessUsersStore->storeBusinessUsersDetail();
$bool = $businessUsersStore->storeBusinessCompleted();
$this->logExecutionTime('Period BusinessStore: '.$bool);
$this->logMemoryUsage("After month {$i}");
}
} catch (\Exception $e) {
$this->error('Error in storeBusinessStructureUsersDetailPeriod: '.$e->getMessage());
throw $e;
}
}
/**
* Löscht gespeicherte Business Structure Daten für den angegebenen Monat/Jahr
*/
private function clearStoredData()
{
try {
$this->info("Clearing stored business data for month: {$this->month} | year: {$this->year}");
// Finde bestehende UserBusinessStructure
$existingStructure = UserBusinessStructure::where('year', $this->year)
->where('month', $this->month)
->first();
if (! $existingStructure) {
$this->info('No stored business structure found to clear');
return;
}
$structureId = $existingStructure->id;
$this->info("Found existing structure with ID: {$structureId}");
// Lösche zugehörige UserBusiness Einträge
$deletedUserBusinesses = UserBusiness::where('b_structure_id', $structureId)->count();
if ($deletedUserBusinesses > 0) {
UserBusiness::where('b_structure_id', $structureId)->delete();
$this->info("Deleted {$deletedUserBusinesses} UserBusiness records");
}
// Lösche die UserBusinessStructure
$existingStructure->delete();
$this->info("Deleted UserBusinessStructure with ID: {$structureId}");
// Garbage Collection nach dem Löschen
gc_collect_cycles();
$this->info('Successfully cleared all stored business data');
$this->logMemoryUsage('After clearing data');
} catch (\Exception $e) {
$this->error('Error clearing stored data: '.$e->getMessage());
throw $e;
}
}
/**
* Schreibt ausgewählten Usern einmal pro Monat ihre Level-qual_kp als KP-Bonus gut.
* Idempotent: Ein bereits vorhandener Eintrag für diesen Monat/Jahr wird übersprungen.
*
* @var array<int> User-IDs die den monatlichen Qual-KP-Bonus erhalten sollen
*/
private function assignMonthlyQualKpBonusPoints(): void
{
$bonusUserIds = [486];
$month = date('m');
$year = date('Y');
$users = User::query()
->whereIn('id', $bonusUserIds)
->whereNotNull('m_level')
->with('user_level')
->get()
->filter(fn (User $user) => $user->user_level && $user->user_level->qual_kp > 0);
$assigned = 0;
$skipped = 0;
foreach ($users as $user) {
$alreadyExists = UserSalesVolume::where('user_id', $user->id)
->where('month', $month)
->where('year', $year)
->where('info', 'qual_kp_bonus')
->exists();
if ($alreadyExists) {
$skipped++;
continue;
}
SalesPointsVolume::addSalesPointsVolume([
'user_id' => $user->id,
'points' => $user->user_level->qual_kp,
'status_points' => 2,
'total_net' => 0,
'status_turnover' => 1,
'info' => 'qual_kp_bonus',
]);
$assigned++;
}
$this->info("Qual-KP Bonus: {$assigned} zugewiesen, {$skipped} übersprungen (bereits vorhanden)");
\Log::channel('cron')->info("Qual-KP Bonus Points: assigned={$assigned}, skipped={$skipped}");
}
private function logExecutionTime($message)
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info($message.' | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms');
}
/**
* Führt eine Funktion mit Fehlerbehandlung aus
*/
private function executeWithErrorHandling(string $processName, callable $callback): void
{
try {
$startTime = microtime(true);
$this->info("Starting: {$processName}");
$this->logMemoryUsage("Before {$processName}");
$callback();
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$this->info("Completed: {$processName} in {$duration}ms");
$this->logMemoryUsage("After {$processName}");
} catch (\Exception $e) {
$this->error("Error in {$processName}: ".$e->getMessage());
$this->error('Stack trace: '.$e->getTraceAsString());
throw $e;
}
}
/**
* Loggt aktuelle Memory-Nutzung
*/
private function logMemoryUsage(string $checkpoint): void
{
$currentMemory = memory_get_usage();
$peakMemory = memory_get_peak_usage();
$memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit'));
$currentFormatted = $this->formatBytes($currentMemory);
$peakFormatted = $this->formatBytes($peakMemory);
$limitFormatted = $this->formatBytes($memoryLimit);
$usagePercent = round(($currentMemory / $memoryLimit) * 100, 2);
$this->info("[{$checkpoint}] Memory: {$currentFormatted} / {$limitFormatted} ({$usagePercent}%) | Peak: {$peakFormatted}");
if ($usagePercent > 80) {
$this->warn("High memory usage detected at {$checkpoint}: {$usagePercent}%");
}
}
/**
* Konvertiert Memory-Limit String zu Bytes
*/
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit) - 1]);
$number = (int) $limit;
switch ($last) {
case 'g':
$number *= 1024;
case 'm':
$number *= 1024;
case 'k':
$number *= 1024;
}
return $number;
}
/**
* Formatiert Bytes in lesbare Einheiten
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision).' '.$units[$i];
}
}

View file

@ -1,185 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Cron\UserPaymentCredits;
use App\Models\UserBusiness;
use App\Services\BusinessPlan\BusinessUserItemOptimized;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\User;
use Illuminate\Console\Command;
use stdClass;
class BusinessTestAccount extends Command
{
/**
* php artisan business:test-account {user_id} {month} {year}
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'business:test-account {user_id} {month} {year} {--commissions : Calculate and show user commissions}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test account data loading for a specific user in business calculations, with optional commission calculation';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$userId = (int) $this->argument('user_id');
$month = (int) $this->argument('month');
$year = (int) $this->argument('year');
$this->info("Testing account data for User ID: {$userId}, Month: {$month}, Year: {$year}");
$this->line('');
// Lade User mit Account
$user = User::with('account', 'user_level')->find($userId);
if (!$user) {
$this->error("User {$userId} not found");
return 1;
}
$this->info("User found: {$user->email}");
$this->info("Account ID: " . ($user->account_id ?? 'NULL'));
if ($user->account) {
$this->info("Account loaded: YES");
$this->info("Account m_account: " . ($user->account->m_account ?? 'NULL'));
$this->info("Account first_name: " . ($user->account->first_name ?? 'NULL'));
$this->info("Account last_name: " . ($user->account->last_name ?? 'NULL'));
$this->info("Account birthday: " . ($user->account->birthday ?? 'NULL'));
$this->info("Account phone: " . ($user->account->getPhoneNumber() ?? 'NULL'));
} else {
$this->warn("Account loaded: NO");
}
$this->line('');
$this->info('Testing BusinessUserItemOptimized...');
// Erstelle Date Object
$date = new stdClass();
$date->month = $month;
$date->year = $year;
$date->start_date = "{$year}-{$month}-01 00:00:00";
$date->end_date = date('Y-m-t 23:59:59', strtotime("{$year}-{$month}-01"));
// Teste BusinessUserItemOptimized
$TreeCalcBot = new TreeCalcBotOptimized($date->month, $date->year, 'admin');
$TreeCalcBot->initBusinesslUserDetail($user, true);
$businessUserItem = $TreeCalcBot->getItem();
$bUser = $businessUserItem->getBUser();
$this->line('');
$this->info('Results from BusinessUserItemOptimized:');
$this->info("m_account: " . ($bUser->m_account ?? 'NULL'));
$this->info("first_name: " . ($bUser->first_name ?? 'NULL'));
$this->info("last_name: " . ($bUser->last_name ?? 'NULL'));
$this->info("user_birthday: " . ($bUser->user_birthday ?? 'NULL'));
$this->info("user_phone: " . ($bUser->user_phone ?? 'NULL'));
$this->info("email: " . ($bUser->email ?? 'NULL'));
$this->line('');
$this->info('Sales Volume Fields:');
$this->info("sales_volume_KP_points: " . ($bUser->sales_volume_KP_points ?? 'NULL'));
$this->info("sales_volume_TP_points: " . ($bUser->sales_volume_TP_points ?? 'NULL'));
$this->info("sales_volume_points_shop: " . ($bUser->sales_volume_points_shop ?? 'NULL'));
$this->info("sales_volume_points_KP_sum: " . ($bUser->sales_volume_points_KP_sum ?? 'NULL'));
$this->info("sales_volume_points_TP_sum: " . ($bUser->sales_volume_points_TP_sum ?? 'NULL'));
$this->info("sales_volume_total: " . ($bUser->sales_volume_total ?? 'NULL'));
$this->info("sales_volume_total_shop: " . ($bUser->sales_volume_total_shop ?? 'NULL'));
$this->info("sales_volume_total_sum: " . ($bUser->sales_volume_total_sum ?? 'NULL'));
$this->line('');
$this->info('Commission Fields:');
$this->info("payline_points: " . ($bUser->payline_points ?? 'NULL'));
$this->info("commission_pp_total: " . ($bUser->commission_pp_total ?? 'NULL'));
$this->info("commission_shop_sales: " . ($bUser->commission_shop_sales ?? 'NULL'));
$this->info("commission_growth_total: " . ($bUser->commission_growth_total ?? 'NULL'));
// Test UserSalesVolume directly
$this->line('');
$this->info('Testing UserSalesVolume data directly:');
$userSalesVolume = $user->getUserSalesVolume($month, $year, 'first');
if ($userSalesVolume) {
$this->info("UserSalesVolume found: ID {$userSalesVolume->id}");
$this->info("month_KP_points: " . ($userSalesVolume->month_KP_points ?? 'NULL'));
$this->info("month_TP_points: " . ($userSalesVolume->month_TP_points ?? 'NULL'));
$this->info("month_shop_points: " . ($userSalesVolume->month_shop_points ?? 'NULL'));
$this->info("month_total_net: " . ($userSalesVolume->month_total_net ?? 'NULL'));
$this->info("month_shop_total_net: " . ($userSalesVolume->month_shop_total_net ?? 'NULL'));
} else {
$this->warn("No UserSalesVolume found for month {$month}/{$year}");
// Check if any UserSalesVolume exists for this user
$anyVolume = \App\Models\UserSalesVolume::where('user_id', $userId)->orderBy('year', 'desc')->orderBy('month', 'desc')->first();
if ($anyVolume) {
$this->info("Latest UserSalesVolume found: {$anyVolume->month}/{$anyVolume->year}");
} else {
$this->warn("No UserSalesVolume records found for this user at all");
}
}
$this->line('');
// Prüfe ob UserBusiness bereits gespeichert ist
$existingUserBusiness = UserBusiness::where('user_id', $userId)
->where('month', $month)
->where('year', $year)
->first();
if ($existingUserBusiness) {
$this->info('Existing UserBusiness found:');
$this->info("m_account: " . ($existingUserBusiness->m_account ?? 'NULL'));
$this->info("first_name: " . ($existingUserBusiness->first_name ?? 'NULL'));
$this->info("last_name: " . ($existingUserBusiness->last_name ?? 'NULL'));
$this->info("user_birthday: " . ($existingUserBusiness->user_birthday ?? 'NULL'));
$this->info("user_phone: " . ($existingUserBusiness->user_phone ?? 'NULL'));
$this->info("email: " . ($existingUserBusiness->email ?? 'NULL'));
} else {
$this->info('No existing UserBusiness found for this period');
}
if ($this->option('commissions')) {
$this->line('');
$this->info('Testing UserBusiness Commissions to Credit...');
if ($existingUserBusiness) {
try {
$userPaymentCredits = new UserPaymentCredits($month, $year);
$ret = $userPaymentCredits->addUserCreditItem($existingUserBusiness);
$this->info('UserBusinessCredit calculated:');
$this->info('User ID: ' . $ret->user_id);
$this->info('Team Commission: ' . $ret->commission_pp_total);
$this->info('Shop Commission: ' . $ret->commission_shop_sales);
} catch (\Exception $e) {
$this->error('Error calculating commissions: ' . $e->getMessage());
}
} else {
$this->warn('No UserBusiness record found, cannot calculate commissions.');
}
}
$this->line('');
$this->info('✅ Test completed successfully');
return 0;
} catch (\Exception $e) {
$this->error('Test failed with error: ' . $e->getMessage());
$this->error('Stack trace: ' . $e->getTraceAsString());
return 1;
}
}
}

View file

@ -1,430 +0,0 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use App\Models\UserBusiness;
use App\Models\UserLevel;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class BusinessUpdateCalculatedFields extends Command
{
/**
* php artisan business:update-calculated-fields {year} {month?}
*
* Aktualisiert bereits gespeicherte UserBusiness-Einträge mit den neuen berechneten Feldern:
* - calc_qual_kp
* - _calculated_qual_kp in Level-Arrays
* - _calculated_payline_points_qual_kp in Level-Arrays
*
* Wenn kein Monat angegeben wird, werden alle 12 Monate des Jahres aktualisiert.
*
* @var string
*/
protected $signature = 'business:update-calculated-fields {year} {month? : Optional - Wenn nicht angegeben, werden alle Monate des Jahres aktualisiert} {--dry-run : Zeige nur was gemacht werden würde, ohne zu speichern}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aktualisiert gespeicherte UserBusiness-Einträge mit neuen berechneten Feldern für Level-Qualifikationen';
private $timeStart;
private $month;
private $year;
private $isDryRun = false;
private $processAllMonths = false;
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$this->timeStart = microtime(true);
$this->year = (int) $this->argument('year');
$monthArg = $this->argument('month');
$this->isDryRun = $this->option('dry-run');
if ($this->isDryRun) {
$this->warn('DRY RUN MODE - Keine Änderungen werden gespeichert');
}
// Prüfe ob ein Monat angegeben wurde
if ($monthArg === null) {
$this->processAllMonths = true;
$this->info("Starte Update für ALLE MONATE des Jahres: {$this->year}");
$this->info(str_repeat('=', 70));
return $this->processFullYear();
} else {
$this->month = (int) $monthArg;
$this->info("Starte Update für Monat: {$this->month} | Jahr: {$this->year}");
$this->logMemoryUsage('Command Start');
return $this->processSingleMonth();
}
} catch (\Exception $e) {
$this->error('Command failed with error: ' . $e->getMessage());
$this->error('Stack trace: ' . $e->getTraceAsString());
$this->logExecutionTime('COMMAND FAILED');
return 1;
}
}
/**
* Verarbeite alle 12 Monate eines Jahres
*/
private function processFullYear(): int
{
$totalUpdated = 0;
$totalSkipped = 0;
$totalErrors = 0;
$monthsProcessed = 0;
$monthsFailed = 0;
for ($month = 1; $month <= 12; $month++) {
$this->month = $month;
$this->newLine();
$this->info("┌─────────────────────────────────────────────────────────────────────┐");
$this->info("│ Verarbeite Monat: " . str_pad($month, 2, '0', STR_PAD_LEFT) . "/" . $this->year . str_repeat(' ', 51) . "");
$this->info("└─────────────────────────────────────────────────────────────────────┘");
try {
$userBusinesses = $this->getUserBusinesses();
if ($userBusinesses->isEmpty()) {
$this->warn(" Keine UserBusiness-Einträge für Monat {$month}/{$this->year} gefunden - überspringe");
continue;
}
$this->info(" Gefunden: {$userBusinesses->count()} UserBusiness-Einträge");
// Aktualisiere die Einträge
$stats = $this->updateCalculatedFieldsWithStats($userBusinesses);
$totalUpdated += $stats['updated'];
$totalSkipped += $stats['skipped'];
$totalErrors += $stats['errors'];
$monthsProcessed++;
$this->info(" ✓ Monat {$month} abgeschlossen: {$stats['updated']} aktualisiert, {$stats['skipped']} übersprungen, {$stats['errors']} Fehler");
$this->logMemoryUsage("Nach Monat {$month}");
} catch (\Exception $e) {
$monthsFailed++;
$this->error(" ✗ Fehler in Monat {$month}: " . $e->getMessage());
\Log::error("BusinessUpdateCalculatedFields: Error in month {$month}/{$this->year}: " . $e->getMessage());
// Weiter mit nächstem Monat
continue;
}
}
// Zusammenfassung für das ganze Jahr
$this->newLine(2);
$this->info(str_repeat('=', 70));
$this->info("ZUSAMMENFASSUNG FÜR DAS JAHR {$this->year}:");
$this->info(str_repeat('=', 70));
$this->info("Verarbeitete Monate: {$monthsProcessed}/12");
$this->info("Fehlgeschlagene Monate: {$monthsFailed}");
$this->info("Gesamt aktualisiert: {$totalUpdated}");
$this->info("Gesamt übersprungen: {$totalSkipped}");
$this->info("Gesamt Fehler: {$totalErrors}");
$this->info(str_repeat('=', 70));
$this->logExecutionTime('JAHRES-UPDATE ABGESCHLOSSEN');
$this->logMemoryUsage('Command End');
return $monthsFailed > 0 ? 1 : 0;
}
/**
* Verarbeite einen einzelnen Monat
*/
private function processSingleMonth(): int
{
// Hole alle UserBusiness-Einträge für den Monat
$userBusinesses = $this->getUserBusinesses();
if ($userBusinesses->isEmpty()) {
$this->error("Keine UserBusiness-Einträge für Monat {$this->month}/{$this->year} gefunden");
return 1;
}
$this->info("Gefunden: {$userBusinesses->count()} UserBusiness-Einträge");
// Aktualisiere die Einträge
$this->updateCalculatedFields($userBusinesses);
$this->logExecutionTime('UPDATE COMPLETED SUCCESSFULLY');
$this->logMemoryUsage('Command End');
return 0;
}
/**
* Hole alle UserBusiness-Einträge für den Monat
*/
private function getUserBusinesses()
{
return UserBusiness::where('month', $this->month)
->where('year', $this->year)
->orderBy('id', 'asc')
->get();
}
/**
* Aktualisiere die berechneten Felder für alle UserBusiness-Einträge
*/
private function updateCalculatedFields($userBusinesses)
{
$stats = $this->updateCalculatedFieldsWithStats($userBusinesses);
$this->info("Update abgeschlossen:");
$this->info(" - Aktualisiert: {$stats['updated']}");
$this->info(" - Übersprungen: {$stats['skipped']}");
$this->info(" - Fehler: {$stats['errors']}");
}
/**
* Aktualisiere die berechneten Felder und gebe Statistiken zurück
*/
private function updateCalculatedFieldsWithStats($userBusinesses): array
{
$bar = $this->output->createProgressBar($userBusinesses->count());
$bar->start();
$updatedCount = 0;
$skippedCount = 0;
$errorCount = 0;
foreach ($userBusinesses as $userBusiness) {
try {
$updated = $this->updateSingleUserBusiness($userBusiness);
if ($updated) {
$updatedCount++;
} else {
$skippedCount++;
}
$bar->advance();
// Memory-Check alle 100 Einträge (nur wenn nicht ganzes Jahr)
if (!$this->processAllMonths && ($updatedCount + $skippedCount) % 100 === 0) {
$this->logMemoryUsage("Nach " . ($updatedCount + $skippedCount) . " Einträgen");
}
} catch (\Exception $e) {
$errorCount++;
$this->newLine();
$this->warn("Fehler bei UserBusiness ID {$userBusiness->id}: " . $e->getMessage());
\Log::error("BusinessUpdateCalculatedFields: Error for UserBusiness {$userBusiness->id}: " . $e->getMessage());
continue;
}
}
$bar->finish();
$this->newLine();
return [
'updated' => $updatedCount,
'skipped' => $skippedCount,
'errors' => $errorCount,
];
}
/**
* Aktualisiere einen einzelnen UserBusiness-Eintrag
*/
private function updateSingleUserBusiness(UserBusiness $userBusiness): bool
{
$hasChanges = false;
// 1. Aktualisiere calc_qual_kp für qual_user_level
if (!$userBusiness->calc_qual_kp) {
$qualKp = $userBusiness->qual_user_level['qual_kp'] ?? null;
if ($qualKp !== null) {
$rest_kp = max(0, $userBusiness->sales_volume_points_KP_sum - $qualKp);
$calc_qual_kp = $rest_kp > 0 ? $qualKp : $userBusiness->sales_volume_points_KP_sum;
if ($userBusiness->calc_qual_kp !== $calc_qual_kp) {
$userBusiness->calc_qual_kp = $calc_qual_kp;
$hasChanges = true;
}
} else {
$userBusiness->calc_qual_kp = $userBusiness->sales_volume_points_KP_sum;
$hasChanges = true;
}
}
// 2. Aktualisiere qual_user_level_next
if (!empty($userBusiness->qual_user_level_next) && is_array($userBusiness->qual_user_level_next)) {
$levelData = $userBusiness->qual_user_level_next;
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
if ($updated !== false) {
$userBusiness->qual_user_level_next = $updated;
$hasChanges = true;
}
}
// 3. Aktualisiere next_qual_user_level
if (!empty($userBusiness->next_qual_user_level) && is_array($userBusiness->next_qual_user_level)) {
$levelData = $userBusiness->next_qual_user_level;
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
if ($updated !== false) {
$userBusiness->next_qual_user_level = $updated;
$hasChanges = true;
}
}
// 4. Aktualisiere next_can_user_level
if (!empty($userBusiness->next_can_user_level) && is_array($userBusiness->next_can_user_level)) {
$levelData = $userBusiness->next_can_user_level;
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
if ($updated !== false) {
$userBusiness->next_can_user_level = $updated;
$hasChanges = true;
}
}
// Speichere nur wenn Änderungen vorhanden sind und nicht im Dry-Run Mode
if ($hasChanges && !$this->isDryRun) {
$userBusiness->save();
}
return $hasChanges;
}
/**
* Füge berechnete Felder zu einem Level-Array hinzu
*
* @return array|false Array mit neuen Feldern oder false wenn keine Änderungen
*/
private function addCalculatedFieldsToLevel(array $levelData, UserBusiness $userBusiness)
{
// Prüfe ob Felder bereits existieren
if (isset($levelData['_calculated_qual_kp']) && isset($levelData['_calculated_payline_points_qual_kp'])) {
return false; // Keine Änderungen nötig
}
$qualKp = $levelData['qual_kp'] ?? null;
$paylines = $levelData['paylines'] ?? 0;
if ($qualKp === null) {
return false;
}
// Berechne die Werte
$payline_points = $this->getPointsForPayline($userBusiness, $paylines);
$rest_kp = max(0, $userBusiness->sales_volume_points_KP_sum - $qualKp);
$payline_points_qual_kp = $payline_points + $rest_kp;
$calc_qual_kp = $rest_kp > 0 ? $qualKp : $userBusiness->sales_volume_points_KP_sum;
// Füge die berechneten Felder hinzu
$levelData['_calculated_qual_kp'] = $calc_qual_kp;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
return $levelData;
}
/**
* Berechne Payline-Punkte für eine bestimmte Anzahl von Paylines
*/
private function getPointsForPayline(UserBusiness $userBusiness, int $paylines): float
{
$payline_points = 0;
$businessLines = $userBusiness->business_lines ?? [];
for ($i = 1; $i <= $paylines; $i++) {
if (isset($businessLines[$i])) {
$line = $businessLines[$i];
// Handle both array and object types
if (is_array($line)) {
$payline_points += (float) ($line['points'] ?? 0);
} elseif (is_object($line)) {
$payline_points += (float) ($line->points ?? 0);
}
}
}
return $payline_points;
}
/**
* Logge Ausführungszeit
*/
private function logExecutionTime($message)
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info($message . ' | Zeit: ' . $sec . 'sec :' . round($micro * 1000, 4) . " ms");
}
/**
* Logge Memory-Nutzung
*/
private function logMemoryUsage(string $checkpoint): void
{
$currentMemory = memory_get_usage();
$peakMemory = memory_get_peak_usage();
$memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit'));
$currentFormatted = $this->formatBytes($currentMemory);
$peakFormatted = $this->formatBytes($peakMemory);
$limitFormatted = $this->formatBytes($memoryLimit);
$usagePercent = round(($currentMemory / $memoryLimit) * 100, 2);
$this->info("[{$checkpoint}] Memory: {$currentFormatted} / {$limitFormatted} ({$usagePercent}%) | Peak: {$peakFormatted}");
if ($usagePercent > 80) {
$this->warn("Hohe Memory-Nutzung bei {$checkpoint}: {$usagePercent}%");
}
}
/**
* Konvertiert Memory-Limit String zu Bytes
*/
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit) - 1]);
$number = (int) $limit;
switch ($last) {
case 'g':
$number *= 1024;
case 'm':
$number *= 1024;
case 'k':
$number *= 1024;
}
return $number;
}
/**
* Formatiert Bytes in lesbare Einheiten
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = array('B', 'KB', 'MB', 'GB', 'TB');
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}

View file

@ -1,213 +0,0 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use App\User;
use App\Services\Util;
use App\Models\UserHistory;
use App\Models\UserMessage;
use App\Mail\MailCustomMessage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class CheckPaymentsAccount extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payments:check-accounts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Überprüft Benutzer-Zahlungskonten und sendet Erinnerungen basierend auf Erneuerungsdaten.';
/**
* Execute the console command.
*/
public function handle()
{
Log::channel('cron')->info('COMMAND [payments:check-accounts] started.');
$this->info('COMMAND [payments:check-accounts] started.');
// Die Logik wurde 1:1 aus der checkPaymentsAccounts-Methode übernommen
$renewalDate = Carbon::now()->modify('+' . (config('mivita.remind_first_days') + 1) . ' days');
Log::channel('cron')->info('Erneuerungsdatum für Zahlungen: ' . $renewalDate->format('Y-m-d H:i:s'));
$users = User::where('payment_account', '!=', NULL)
->where('active', '=', 1)
->where('blocked', '!=', 1)
->where('payment_account', '<', $renewalDate)
->get();
Log::channel('cron')->info('Found ' . $users->count() . ' users for payment reminders.');
$this->info('Found ' . $users->count() . ' users for payment reminders.');
foreach ($users as $user) {
Log::channel('cron')->info('Prüfe Zahlungserinnerungen für Benutzer: ' . $user->email);
$this->checkReminderPayments($user);
}
Log::channel('cron')->info('COMMAND [payments:check-accounts] finished.');
$this->info('COMMAND [payments:check-accounts] finished.');
return 0; // Success
}
/**
* Überprüft und sendet Zahlungserinnerungen basierend auf Benutzerkontostand
*
* RULES:
* > 21 remind_first_days = 31 reminder_first
* > 21 remind_first_days + sepa = 32 reminder_first_sepa
* > 14 remind_sec_days = 33 reminder_sec
* > 2 remind_last_days = 34 reminder_last
* > 0 deaktiv = 35 reminder_deaktiv
* > 0 deaktiv + sepa = 36 reminder_deaktiv_sepa
* == 7 abo_booking_days + sepa + cron = 37 reminder_collect_sepa
*
* @param User $user Benutzer
* @return void
*/
private function checkReminderPayments(User $user)
{
//35 reminder_deaktiv, 36 reminder_deaktiv_sepa
if (!$user->isActiveAccount()) {
Log::channel('cron')->info('Inaktives Konto für Benutzer: ' . $user->email);
$this->checkIsReminderSend($user, 35);
return;
}
//34 reminder_last
if ($user->daysActiveAccount() <= config('mivita.remind_last_days')) {
Log::channel('cron')->info('Letzte Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 34);
return;
}
//33 reminder_sec
if ($user->daysActiveAccount() <= config('mivita.remind_sec_days')) {
Log::channel('cron')->info('Zweite Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 33);
return;
}
//31 reminder_first
if ($user->daysActiveAccount() > config('mivita.remind_sec_days')) {
Log::channel('cron')->info('Erste Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 31);
return;
}
}
/**
* Überprüft, ob eine Erinnerung bereits gesendet wurde
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return bool
*/
private function checkIsReminderSend(User $user, $status)
{
$isSend = UserHistory::whereUserId($user->id)
->whereAction('reminder_payments')
->whereIdentifier($user->payment_account)
->whereStatus($status)
->latest()
->first();
if ($isSend) {
Log::channel('cron')->info('Erinnerung bereits gesendet für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
return true;
}
Log::channel('cron')->info('Sende neue Erinnerung für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
$referenz = $this->sendReminderMail($user, $status);
UserHistory::create([
'user_id' => $user->id,
'action' => 'reminder_payments',
'referenz' => $referenz,
'identifier' => $user->payment_account,
'status' => $status
]);
return false;
}
/**
* Sendet eine Erinnerungs-E-Mail an den Benutzer
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return int
*/
private function sendReminderMail(User $user, $status)
{
$days = abs($user->daysActiveAccount());
$pay_date = Carbon::parse($user->payment_account)->modify('- ' . config('mivita.abo_booking_days') . ' days')->format('d.m.Y');
$datetime = $user->getPaymentAccountDateFormat();
$price = "";
if ($user->payment_order_id && isset($user->payment_order_product->price)) {
$price = 'von ' . $user->payment_order_product->getFormattedPrice() . ' EUR';
}
$message = __('reminder.copy_first_' . $status, ['days' => $days, 'datetime' => $datetime, 'price' => $price, 'pay_date' => $pay_date]);
$message_last = __('reminder.copy_last_' . $status, ['days' => $days, 'datetime' => $datetime, 'price' => $price, 'pay_date' => $pay_date]);
$button = __('reminder.button_' . $status);
$message = preg_replace("/[\n\r]/", "", $message);
$message_last = preg_replace("/[\n\r]/", "", $message_last);
$data = [
'subject' => __('reminder.subject') . " | ID: " . $status,
'message' => $message,
'message_last' => $message_last,
'url' => config('app.url') . '/user/membership',
'button' => $button,
];
$sender = User::find(1);
$customer_mail = UserMessage::create(['user_id' => $user->id, 'send_user_id' => $sender->id, 'email' => $user->email, 'subject' => $data['subject'], 'message' => $data['message'] . " " . $data['message_last']]);
try {
if (!Util::isTestSystem()) {
if ($status >= 34) {
Log::channel('cron')->info('Sende kritische Erinnerung mit BCC an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->bcc(config('app.default_mail'))
->send(new MailCustomMessage($user, $data, $sender, false));
} else {
Log::channel('cron')->info('Sende normale Erinnerung an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->send(new MailCustomMessage($user, $data, $sender, false));
}
} else {
Log::channel('cron')->info('Testsystem: E-Mail-Versand simuliert für: ' . $user->email);
}
} catch (\Exception $e) {
Log::channel('cron')->error('Mail-Fehler für Benutzer ' . $user->email . ': ' . $e->getMessage());
$customer_mail->fail = true;
$customer_mail->error = $e->getMessage();
$customer_mail->save();
return 0;
}
$customer_mail->send = true;
$customer_mail->sent_at = now();
$customer_mail->save();
Log::channel('cron')->info('Erinnerungsmail erfolgreich gesendet an: ' . $user->email);
return 1;
}
}

View file

@ -1,97 +0,0 @@
<?php
namespace App\Console\Commands;
use Acme\Dhl\Models\DhlShipment;
use Illuminate\Console\Command;
class DhlBackfillEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dhl:backfill-emails
{--dry-run : Nur simulieren, keine Änderungen}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Füllt das E-Mail-Feld für bestehende DHL-Sendungen nach';
/**
* Execute the console command.
*/
public function handle(): int
{
$dryRun = $this->option('dry-run');
$this->info('DHL E-Mail Backfill gestartet');
$this->info('Modus: ' . ($dryRun ? 'DRY-RUN (keine Änderungen)' : 'LIVE'));
$this->newLine();
// Hole alle Sendungen ohne E-Mail
$shipments = DhlShipment::with('shoppingOrder.shopping_user')
->whereNull('email')
->orWhere('email', '')
->get();
$total = $shipments->count();
$updated = 0;
$skipped = 0;
$this->info("Gefundene Sendungen ohne E-Mail: {$total}");
$this->newLine();
$bar = $this->output->createProgressBar($total);
foreach ($shipments as $shipment) {
$bar->advance();
// Hole E-Mail aus Shopping User
$email = null;
if ($shipment->shoppingOrder && $shipment->shoppingOrder->shopping_user) {
$email = $shipment->shoppingOrder->shopping_user->email;
}
if (empty($email)) {
$skipped++;
continue;
}
if (! $dryRun) {
$shipment->email = $email;
$shipment->save();
}
$updated++;
}
$bar->finish();
$this->newLine(2);
// Statistik
$this->info('Backfill abgeschlossen!');
$this->newLine();
$this->table(
['Metrik', 'Anzahl'],
[
['Gesamt geprüft', $total],
['E-Mail gesetzt', $updated],
['Übersprungen (keine E-Mail)', $skipped],
]
);
if ($dryRun) {
$this->warn('DRY-RUN: Keine Änderungen wurden vorgenommen.');
$this->info('Führen Sie den Befehl ohne --dry-run aus, um die Änderungen zu speichern.');
} else {
$this->info("{$updated} Sendungen wurden aktualisiert.");
}
return Command::SUCCESS;
}
}

View file

@ -1,323 +0,0 @@
<?php
namespace App\Console\Commands;
use Acme\Dhl\Models\DhlShipment;
use App\Mail\MailDhlTracking;
use App\Services\DhlTrackingService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class DhlUpdateTracking extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dhl:update-tracking
{--days=30 : Sendungen der letzten X Tage aktualisieren}
{--send-emails : Automatisch E-Mails bei Transit-Status senden}
{--dry-run : Nur simulieren, keine Änderungen}
{--test-email= : Test-E-Mail an angegebene Adresse senden}
{--order= : Nur für bestimmte Bestellung (Order-ID)}
{--force : Intervall-Filter überspringen, alle aktiven Sendungen aktualisieren}
{--stale-days=30 : Sendungen ohne Statusänderung nach X Tagen als abgeschlossen markieren}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aktualisiert Tracking-Status für DHL Sendungen (status-basierte Intervalle, Batch-API)';
/**
* Execute the console command.
*/
public function handle(): int
{
$days = (int) $this->option('days');
$sendEmails = $this->option('send-emails');
$dryRun = $this->option('dry-run');
$testEmail = $this->option('test-email');
$orderId = $this->option('order');
$force = $this->option('force');
$staleDays = (int) $this->option('stale-days');
$this->info('DHL Tracking Update gestartet');
$this->info("Optionen: --days={$days}, --send-emails=".($sendEmails ? 'ja' : 'nein')
.', --dry-run='.($dryRun ? 'ja' : 'nein')
.', --force='.($force ? 'ja' : 'nein')
.", --stale-days={$staleDays}");
if ($testEmail) {
$this->info("Test-Modus: E-Mails werden an {$testEmail} gesendet");
}
if ($orderId) {
$this->info("Filter: Nur Order-ID {$orderId}");
}
$this->newLine();
// Step 1: Mark stale shipments as completed (before main query)
$staleCompleted = $this->markStaleShipmentsCompleted($staleDays, $dryRun);
// Step 2: Build query for shipments that need tracking update
$query = $this->buildShipmentQuery($days, $orderId, $force);
$shipments = $query->orderBy('created_at', 'desc')->get();
// Count total active shipments for statistics (before interval filter)
$totalActive = DhlShipment::active()
->whereNull('tracking_completed_at')
->where('created_at', '>=', now()->subDays($days))
->whereNotNull('dhl_shipment_no')
->count();
$total = $shipments->count();
$skippedByInterval = $totalActive - $total;
$this->info("Aktive Sendungen gesamt: {$totalActive}");
$this->info('Übersprungen (Intervall): '.max(0, $skippedByInterval));
$this->info("Zu aktualisieren: {$total}");
if ($total === 0) {
$this->info('Keine Sendungen zum Aktualisieren gefunden.');
$this->printSummary(0, ['updated' => 0, 'failed' => 0, 'completed' => 0, 'emails_sent' => 0, 'skipped' => 0], $staleCompleted, max(0, $skippedByInterval));
return self::SUCCESS;
}
$trackingService = new DhlTrackingService;
$stats = [
'updated' => 0,
'failed' => 0,
'completed' => 0,
'emails_sent' => 0,
'skipped' => 0,
];
if ($dryRun) {
$stats['skipped'] = $total;
$this->info("Dry-Run: {$total} Sendungen würden aktualisiert.");
} else {
// Collect old statuses for email decision
$oldStatuses = $shipments->pluck('status', 'id')->toArray();
// Use batch API for efficient processing
$this->info('Starte Batch-Tracking-Update...');
$bar = $this->output->createProgressBar($total);
$bar->start();
$batchResult = $trackingService->updateTrackingBatch($shipments);
$stats['updated'] = $batchResult['updated'];
$stats['failed'] = $batchResult['failed'];
$stats['completed'] = $batchResult['completed'];
$bar->advance($total);
$bar->finish();
$this->newLine(2);
// Send tracking emails if enabled
if ($sendEmails) {
$this->info('Prüfe E-Mail-Versand...');
foreach ($shipments as $shipment) {
$shipment->refresh();
$oldStatus = $oldStatuses[$shipment->id] ?? '';
if ($this->shouldSendEmail($shipment, $oldStatus)) {
try {
$this->sendTrackingEmail($shipment, $testEmail);
$stats['emails_sent']++;
} catch (\Exception $e) {
Log::error('[DHL Cron] Failed to send tracking email', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}
}
}
$this->printSummary($total, $stats, $staleCompleted, max(0, $skippedByInterval));
Log::info('[DHL Cron] Tracking update completed', array_merge($stats, [
'total' => $total,
'stale_completed' => $staleCompleted,
'skipped_interval' => max(0, $skippedByInterval),
]));
return self::SUCCESS;
}
/**
* Build the shipment query with or without interval filtering.
*/
private function buildShipmentQuery(int $days, ?string $orderId, bool $force)
{
if ($force) {
// --force: Alle aktiven Sendungen ohne Intervall-Filter
$query = DhlShipment::active()
->whereNull('tracking_completed_at')
->where('created_at', '>=', now()->subDays($days))
->whereNotNull('dhl_shipment_no');
} else {
// Normal: Status-basierte Intervalle beachten
$query = DhlShipment::needsTrackingUpdate()
->where('created_at', '>=', now()->subDays($days));
}
// Filter nach Order-ID wenn angegeben
if ($orderId) {
$query->where('order_id', $orderId);
}
return $query;
}
/**
* Mark shipments as tracking-completed if they haven't changed status
* for a given number of days (stale shipments).
*/
private function markStaleShipmentsCompleted(int $staleDays, bool $dryRun): int
{
$staleShipments = DhlShipment::active()
->whereNull('tracking_completed_at')
->whereNotNull('last_tracked_at')
->where('last_tracked_at', '<', now()->subDays($staleDays))
->where('created_at', '<', now()->subDays($staleDays))
->get();
$count = $staleShipments->count();
if ($count > 0) {
$this->warn("Veraltete Sendungen gefunden: {$count} (>{$staleDays} Tage ohne Änderung)");
if (! $dryRun) {
foreach ($staleShipments as $shipment) {
$shipment->markTrackingCompleted();
Log::info('[DHL Cron] Stale shipment tracking completed', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'last_tracked_at' => $shipment->last_tracked_at?->toDateTimeString(),
]);
}
$this->info("{$count} Sendungen als Tracking-abgeschlossen markiert.");
} else {
$this->info(" → Dry-Run: {$count} Sendungen würden als abgeschlossen markiert.");
}
}
$this->newLine();
return $count;
}
/**
* Print the final summary table.
*/
private function printSummary(int $total, array $stats, int $staleCompleted, int $skippedByInterval): void
{
$this->info('Zusammenfassung:');
$this->table(
['Metrik', 'Anzahl'],
[
['Zu aktualisieren', $total],
['Aktualisiert', $stats['updated']],
['Fehlgeschlagen', $stats['failed']],
['Tracking abgeschlossen', $stats['completed']],
['E-Mails gesendet', $stats['emails_sent']],
['Übersprungen (Dry-Run)', $stats['skipped']],
['Übersprungen (Intervall)', $skippedByInterval],
['Veraltet → abgeschlossen', $staleCompleted],
]
);
}
/**
* Prüft ob eine E-Mail gesendet werden soll
*/
private function shouldSendEmail(DhlShipment $shipment, string $oldStatus): bool
{
// E-Mail nur senden wenn:
// 1. Status ist jetzt "in_transit"
// 2. Vorheriger Status war NICHT "in_transit" (also Status hat sich geändert)
// 3. Noch keine E-Mail gesendet wurde
return $shipment->status === 'in_transit'
&& $oldStatus !== 'in_transit'
&& ! $shipment->wasTrackingEmailSent()
&& $shipment->canSendTrackingEmail();
}
/**
* Sendet die Tracking-E-Mail (mit Unterstützung für mehrere Sendungen pro Bestellung)
*/
private function sendTrackingEmail(DhlShipment $shipment, ?string $testEmail = null): void
{
try {
$order = $shipment->shoppingOrder;
// Determine recipient email: test email > shipment email > shopping user email
$recipientEmail = null;
if ($testEmail) {
$recipientEmail = $testEmail;
} elseif (! empty($shipment->email)) {
$recipientEmail = $shipment->email;
} elseif ($order->shopping_user && ! empty($order->shopping_user->email)) {
$recipientEmail = $order->shopping_user->email;
}
if (! $recipientEmail) {
Log::warning('[DHL Cron] Cannot send email - no recipient', [
'shipment_id' => $shipment->id,
]);
return;
}
// Sammle alle Sendungen für diese Bestellung, die noch keine E-Mail erhalten haben
$allShipments = DhlShipment::where('order_id', $order->id)
->where('status', 'in_transit')
->whereNotNull('dhl_shipment_no')
->whereNull('tracking_email_sent_at')
->get();
// Wenn keine Sendungen gefunden, nutze nur die aktuelle
if ($allShipments->isEmpty()) {
$allShipments = collect([$shipment]);
}
// Sende E-Mail mit allen Sendungen
Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order));
// Markiere alle Sendungen als versendet
foreach ($allShipments as $s) {
$s->markTrackingEmailSent('auto');
}
Log::info('[DHL Cron] Tracking email sent automatically', [
'shipment_ids' => $allShipments->pluck('id')->toArray(),
'shipments_count' => $allShipments->count(),
'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(),
'email' => $recipientEmail,
'is_test' => ! is_null($testEmail),
]);
if ($allShipments->count() > 1) {
$this->line(" → E-Mail mit {$allShipments->count()} Sendungen gesendet an: {$recipientEmail}");
} else {
$this->line(" → E-Mail gesendet an: {$recipientEmail}");
}
} catch (\Exception $e) {
Log::error('[DHL Cron] Failed to send tracking email', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}

View file

@ -1,119 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\ShoppingInstance;
use App\Models\ShoppingPayment;
use Illuminate\Console\Command;
class FixPaymentLinkStatus extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payment:fix-link-status {--dry-run : Run without making changes}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fix payment link status for paid orders that have incorrect status in shopping_instances';
/**
* Execute the console command.
*/
public function handle()
{
$isDryRun = $this->option('dry-run');
if ($isDryRun) {
$this->info(' DRY RUN MODE - No changes will be made');
$this->newLine();
}
$this->info('🔎 Searching for payment links with incorrect status...');
$this->newLine();
// Find all ShoppingPayments with identifier that are paid
$paidPayments = ShoppingPayment::whereNotNull('identifier')
->whereHas('shopping_order', function ($query) {
$query->where('paid', 1)
->where('txaction', 'paid');
})
->with(['shopping_order'])
->get();
$this->info("Found {$paidPayments->count()} paid payments with identifiers");
$this->newLine();
$fixed = 0;
$skipped = 0;
$errors = 0;
foreach ($paidPayments as $payment) {
$identifier = $payment->identifier;
// Find the corresponding ShoppingInstance
$instance = ShoppingInstance::where('identifier', $identifier)->first();
if (! $instance) {
$this->warn("⚠️ ShoppingInstance not found for identifier: {$identifier}");
$errors++;
continue;
}
// Check if status needs to be updated
if ($instance->status < 10) {
$oldStatus = $instance->status;
$oldStatusName = $instance->getStatus();
if (! $isDryRun) {
$instance->status = 10; // link_paid
$instance->save();
}
$this->line(sprintf(
'%s Payment #%d: %s → %s (Order #%d, Amount: %s)',
$isDryRun ? '📋' : '✅',
$payment->id,
$oldStatusName." ($oldStatus)",
'link_paid (10)',
$payment->shopping_order_id,
$payment->getPaymentAmount()
));
$fixed++;
} else {
$skipped++;
}
}
$this->newLine();
$this->info('📊 Summary:');
$this->table(
['Status', 'Count'],
[
['Fixed/Would fix', $fixed],
['Already correct', $skipped],
['Errors', $errors],
['Total processed', $paidPayments->count()],
]
);
if ($isDryRun && $fixed > 0) {
$this->newLine();
$this->warn('⚠️ This was a DRY RUN. Run without --dry-run to apply changes.');
}
if (! $isDryRun && $fixed > 0) {
$this->newLine();
$this->info("✨ Successfully updated {$fixed} payment link(s)!");
}
return 0;
}
}

View file

@ -1,205 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\Services\Incentive\IncentivePointsLogRepairService;
use App\Services\Incentive\IncentiveTracker;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/**
* Batch-Neuberechnung fuer Incentives.
*
* Abo-Wertung (nicht hier codiert, sondern in {@see IncentiveParticipant::rebuildFromSourceTables}
* und {@see IncentivePointsLogRepairService::syncMissingTrackingAbos}):
*
* - Eigenabo (me): zaehlt auch wenn es vor dem Qualifikationszeitraum abgeschlossen wurde;
* Einmalpunkte wirken dann ab Qualifikationsbeginn (aktiviert_at/Log-Monat auf Start des Zeitraums).
* - Kundenabo (ot): nur wenn im Qualifikationszeitraum neu abgeschlossen (created_at im Zeitraum).
*
* Zu Beginn werden fuer alle Berater (User mit m_level) fehlende Teilnehmerzeilen ohne
* accepted_terms angelegt ({@see IncentiveParticipant::ensureConsultantsForIncentive}), damit
* Punkte ohne Checkbox mitlaufen; die Rangliste blendet Namen erst nach Zustimmung ein.
*/
class IncentiveCalculate extends Command
{
protected $signature = 'incentive:calculate
{incentive_id? : ID des Incentives (leer = alle aktiven)}
{--force : Tracking-Tabellen + Log loeschen und komplett neu aufbauen}
{--skip-repair : Kein Nachziehen von Trackings/FKs/SV-Logs (nur Summen aus bestehendem Log)}
{--verbose-details : Zeigt Details pro Teilnehmer}';
protected $description = 'Incentive-Punkte: fehlende Partner-/Abo-Trackings, FK-Reparatur, fehlende SV-Logs, Summen/Ranking; --force = kompletter Neuaufbau aus Quelldaten';
public function handle(IncentivePointsLogRepairService $repairService): int
{
if ($id = $this->argument('incentive_id')) {
$incentive = Incentive::find($id);
if (! $incentive) {
$this->error("Incentive #{$id} nicht gefunden.");
return self::FAILURE;
}
return $this->processIncentive($incentive, $repairService);
}
$incentives = Incentive::active()->get();
if ($incentives->isEmpty()) {
$this->info('Keine aktiven Incentives gefunden.');
return self::SUCCESS;
}
$exitCode = self::SUCCESS;
foreach ($incentives as $incentive) {
if ($this->processIncentive($incentive, $repairService) !== self::SUCCESS) {
$exitCode = self::FAILURE;
}
}
return $exitCode;
}
private function processIncentive(Incentive $incentive, IncentivePointsLogRepairService $repairService): int
{
$force = $this->option('force');
$skipRepair = $this->option('skip-repair');
$verbose = $this->option('verbose-details');
$this->info("=== {$incentive->name} (ID: {$incentive->id}) ===");
$this->info(" Zeitraum: {$incentive->qualification_start->format('d.m.Y')} - {$incentive->qualification_end->format('d.m.Y')}");
if ($force) {
$this->info(' Modus: FORCE (Tracking + Log aus Quelldaten neu aufbauen)');
} elseif ($skipRepair) {
$this->info(' Modus: Nur Neuberechnung (Summen/Ranking aus bestehendem Log)');
} else {
$this->info(' Modus: Tracking nachziehen + FK-Reparatur + SV-Logs + Neuberechnung');
}
$stubAdded = IncentiveParticipant::ensureConsultantsForIncentive($incentive);
if ($stubAdded > 0) {
$this->info(" Berater-Teilnehmer neu angelegt (ohne Zustimmung): {$stubAdded}");
}
$participants = $incentive->participants()->with('user', 'user.account')->get();
$this->info(" Teilnehmer: {$participants->count()}");
$this->newLine();
$stats = [
'processed' => 0,
'errors' => 0,
'with_points' => 0,
'with_partners' => 0,
'with_abos' => 0,
'tracking_partner_added' => 0,
'tracking_abo_added' => 0,
'repair_partner_fk' => 0,
'repair_abo_fk' => 0,
'repair_onetime_partner_fk' => 0,
'repair_onetime_abo_fk' => 0,
'sv_logs_added' => 0,
];
$errors = [];
$bar = $this->output->createProgressBar($participants->count());
$bar->start();
foreach ($participants as $participant) {
try {
if (! $participant->user) {
$bar->advance();
continue;
}
if ($force) {
$participant->rebuildFromSourceTables()->save();
} else {
if (! $skipRepair) {
$stats['tracking_partner_added'] += $repairService->syncMissingTrackingPartners($participant);
$stats['tracking_abo_added'] += $repairService->syncMissingTrackingAbos($participant);
$r = $repairService->repairForeignKeys($participant);
$stats['repair_partner_fk'] += $r['partner_fk'];
$stats['repair_abo_fk'] += $r['abo_fk'];
$stats['repair_onetime_partner_fk'] += $r['onetime_partner_fk'];
$stats['repair_onetime_abo_fk'] += $r['onetime_abo_fk'];
$stats['sv_logs_added'] += $repairService->syncMissingSalesVolumeLogs($participant);
}
$participant->recalculateFromTrackingTables()->save();
}
$stats['processed']++;
if ($participant->total_points > 0) {
$stats['with_points']++;
}
if ($participant->qualified_partners > 0) {
$stats['with_partners']++;
}
if ($participant->qualified_abos > 0) {
$stats['with_abos']++;
}
if ($verbose && $participant->total_points > 0) {
$name = $participant->user->account
? $participant->user->account->first_name.' '.$participant->user->account->last_name
: ($participant->user->email ?? 'User #'.$participant->user_id);
$bar->clear();
$this->line(" {$name}: {$participant->total_points} Pkt, {$participant->qualified_partners} Partner, {$participant->qualified_abos} Abos");
$bar->display();
}
} catch (\Throwable $e) {
$stats['errors']++;
$errors[] = "Participant #{$participant->id} (User #{$participant->user_id}): {$e->getMessage()}";
Log::error('IncentiveCalculation error for participant '.$participant->id.': '.$e->getMessage());
}
$bar->advance();
}
$bar->finish();
$this->newLine(2);
IncentiveTracker::updateRanking($incentive);
$ranked = IncentiveParticipant::where('incentive_id', $incentive->id)
->whereNotNull('rank')
->count();
$tableRows = [
['Verarbeitet', (string) $stats['processed']],
['Fehler', (string) $stats['errors']],
['Mit Punkten', (string) $stats['with_points']],
['Mit Partnern', (string) $stats['with_partners']],
['Mit Abos', (string) $stats['with_abos']],
['Im Ranking', (string) $ranked],
];
if (! $force) {
$tableRows[] = ['Neupartner-Trackings nachgezogen', (string) $stats['tracking_partner_added']];
$tableRows[] = ['Neuabo-Trackings nachgezogen', (string) $stats['tracking_abo_added']];
$tableRows[] = ['FK Partner (akkum.) repariert', (string) $stats['repair_partner_fk']];
$tableRows[] = ['FK Abo (akkum.) repariert', (string) $stats['repair_abo_fk']];
$tableRows[] = ['FK Partner (Einmal) repariert', (string) $stats['repair_onetime_partner_fk']];
$tableRows[] = ['FK Abo (Einmal) repariert', (string) $stats['repair_onetime_abo_fk']];
$tableRows[] = ['Neue SV-Log-Eintraege', (string) $stats['sv_logs_added']];
}
$this->table(['Metrik', 'Wert'], $tableRows);
if (! empty($errors)) {
$this->error('Fehler:');
foreach ($errors as $err) {
$this->line(" - {$err}");
}
return self::FAILURE;
}
$this->info('Fertig.');
return self::SUCCESS;
}
}

View file

@ -1,196 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Incentive;
use App\Models\IncentiveNewPartner;
use App\Models\IncentiveParticipant;
use App\Models\ShoppingOrder;
use App\User;
use Illuminate\Console\Command;
class IncentiveDebugTrackPartner extends Command
{
protected $signature = 'incentive:debug-track-partner {order_id : Shopping Order ID}';
protected $description = 'Debuggt trackNewPartner Schritt fuer Schritt fuer eine bestimmte Bestellung';
public function handle(): int
{
$order_id = $this->argument('order_id');
$shopping_order = ShoppingOrder::find($order_id);
if (! $shopping_order) {
$this->error("Shopping Order #{$order_id} nicht gefunden.");
return self::FAILURE;
}
$this->info("=== Debug trackNewPartner fuer Order #{$order_id} ===");
$this->newLine();
// 1. Bestelldaten
$this->info('[1] Bestelldaten:');
$this->table(['Feld', 'Wert'], [
['id', $shopping_order->id],
['auth_user_id', $shopping_order->auth_user_id ?? 'NULL'],
['member_id', $shopping_order->member_id ?? 'NULL'],
['payment_for', $shopping_order->payment_for],
['paid', $shopping_order->paid],
['txaction', $shopping_order->txaction],
['mode', $shopping_order->mode],
['created_at', $shopping_order->created_at],
]);
// 2. Prüfe payment_for == 1 (Voraussetzung im Payment.php)
if ($shopping_order->payment_for != 1) {
$this->warn("[!] payment_for = {$shopping_order->payment_for} (nicht 1/registration). trackNewPartner wird nur bei payment_for=1 aufgerufen!");
}
// 3. Neuer User
$this->newLine();
$this->info('[2] Neuer User (auth_user_id):');
if (! $shopping_order->auth_user_id) {
$this->error(' auth_user_id ist NULL -> ABBRUCH (return)');
return self::SUCCESS;
}
$new_user = User::find($shopping_order->auth_user_id);
if (! $new_user) {
$this->error(" User #{$shopping_order->auth_user_id} nicht gefunden -> ABBRUCH (return)");
return self::SUCCESS;
}
$this->table(['Feld', 'Wert'], [
['id', $new_user->id],
['email', $new_user->email],
['m_sponsor', $new_user->m_sponsor ?? 'NULL'],
['active', $new_user->active],
['created_at', $new_user->created_at],
]);
if (! $new_user->m_sponsor) {
$this->error(' m_sponsor ist NULL -> ABBRUCH (return)');
return self::SUCCESS;
}
$sponsor_id = $new_user->m_sponsor;
$this->info(" Sponsor ID: {$sponsor_id}");
// 4. Registration Date
$registration_date = $shopping_order->created_at;
$this->newLine();
$this->info("[3] Registration Date: {$registration_date}");
// 5. Aktive Incentives
$this->newLine();
$this->info('[4] Aktive Incentives pruefen:');
$all_incentives = Incentive::query()->get();
$this->info(" Incentives gesamt: {$all_incentives->count()}");
foreach ($all_incentives as $incentive) {
$is_active = $incentive->status == 1;
$in_range = $registration_date >= $incentive->qualification_start
&& $registration_date <= $incentive->qualification_end;
$status_icon = $is_active ? 'AKTIV' : 'INAKTIV';
$range_icon = $in_range ? 'IM ZEITRAUM' : 'AUSSERHALB';
$this->table(['Feld', 'Wert'], [
['Incentive', "#{$incentive->id}: {$incentive->name}"],
['Status', "{$incentive->status} ({$status_icon})"],
['qualification_start', $incentive->qualification_start],
['qualification_end', $incentive->qualification_end],
['Registration Date', "{$registration_date} ({$range_icon})"],
]);
if (! $is_active) {
$this->warn(' -> Uebersprungen: Incentive nicht aktiv');
continue;
}
if (! $in_range) {
$this->warn(' -> Uebersprungen: Registration Date ausserhalb Qualifikationszeitraum');
continue;
}
$this->info(" -> MATCH! Incentive #{$incentive->id} ist aktiv und Registration Date liegt im Zeitraum.");
// 6. Participant prüfen
$this->newLine();
$this->info("[5] Participant-Check: Sponsor #{$sponsor_id} in Incentive #{$incentive->id}");
$participant = IncentiveParticipant::where('incentive_id', $incentive->id)
->where('user_id', $sponsor_id)
->first();
if (! $participant) {
$this->error(" Sponsor #{$sponsor_id} ist KEIN Teilnehmer in Incentive #{$incentive->id} -> SKIP");
// Zeige alle Teilnehmer-User-IDs
$participant_ids = IncentiveParticipant::where('incentive_id', $incentive->id)
->pluck('user_id')
->toArray();
$this->info(' Teilnehmer User-IDs: '.implode(', ', array_slice($participant_ids, 0, 20))
.(count($participant_ids) > 20 ? '... (+'.count($participant_ids) - 20 .')' : ''));
continue;
}
$this->info(" Participant gefunden: #{$participant->id} (User #{$participant->user_id})");
$this->table(['Feld', 'Wert'], [
['participant.id', $participant->id],
['user_id', $participant->user_id],
['total_points', $participant->total_points],
['qualified_partners', $participant->qualified_partners],
['accepted_terms_at', $participant->accepted_terms_at ?? 'NULL'],
]);
// 7. Tracking-Eintrag prüfen
$this->newLine();
$this->info('[6] Tracking-Eintrag (incentive_new_partners):');
$existing = IncentiveNewPartner::where('participant_id', $participant->id)
->where('user_id', $new_user->id)
->first();
if ($existing) {
$this->warn(" Eintrag existiert bereits: #{$existing->id} (erstellt: {$existing->created_at})");
} else {
$this->info(' Kein Eintrag vorhanden -> wuerde neu erstellt werden.');
}
// 8. Zusammenfassung
$this->newLine();
$this->info('=== ERGEBNIS ===');
$this->info('trackNewPartner WUERDE erfolgreich laufen fuer:');
$this->info(" Neuer Partner: User #{$new_user->id} ({$new_user->email})");
$this->info(" Sponsor/Teilnehmer: User #{$sponsor_id} (Participant #{$participant->id})");
$this->info(" Incentive: #{$incentive->id} ({$incentive->name})");
$this->info(" Einmalpunkte: {$incentive->points_partner_onetime}");
}
// Prüfe den Query wie er im Code steht
$this->newLine();
$this->info('[7] Exakter Query wie im Code:');
$matched_incentives = Incentive::query()
->active()
->where('qualification_start', '<=', $registration_date)
->where('qualification_end', '>=', $registration_date)
->get();
$this->info(" Incentive::active()->where(start <= {$registration_date})->where(end >= {$registration_date})");
$this->info(" Ergebnis: {$matched_incentives->count()} Incentive(s)");
foreach ($matched_incentives as $mi) {
$this->info(" -> #{$mi->id}: {$mi->name}");
}
return self::SUCCESS;
}
}

View file

@ -1,241 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveNewPartner;
use App\Models\IncentivePointsLog;
use App\Models\ShoppingOrder;
use App\Models\UserSalesVolume;
use Illuminate\Console\Command;
class IncentiveDebugTrackSalesVolume extends Command
{
protected $signature = 'incentive:debug-track-sv {sv_id : UserSalesVolume ID}';
protected $description = 'Debuggt trackSalesVolume Schritt fuer Schritt fuer einen bestimmten SalesVolume-Eintrag';
public function handle(): int
{
$sv_id = $this->argument('sv_id');
$usv = UserSalesVolume::find($sv_id);
if (! $usv) {
$this->error("UserSalesVolume #{$sv_id} nicht gefunden.");
return self::FAILURE;
}
$this->info("=== Debug trackSalesVolume fuer USV #{$sv_id} ===");
$this->newLine();
// 1. SalesVolume-Daten
$this->info('[1] SalesVolume-Daten:');
$this->table(['Feld', 'Wert'], [
['id', $usv->id],
['user_id', $usv->user_id ?? 'NULL'],
['shopping_order_id', $usv->shopping_order_id ?? 'NULL'],
['user_invoice_id', $usv->user_invoice_id ?? 'NULL'],
['month', $usv->month ?? 'NULL'],
['year', $usv->year ?? 'NULL'],
['points', $usv->getRawOriginal('points') ?? 'NULL'],
['status', $usv->status.' ('.($usv->getStatusType() ?: '-').')'],
['status_points', $usv->status_points ?? 'NULL'],
['status_turnover', $usv->status_turnover ?? 'NULL'],
['message', $usv->message ?? 'NULL'],
]);
// 2. Fruehe Abbruch-Checks
$month = $usv->month;
$year = $usv->year;
if (! $month || ! $year) {
$this->error('[ABBRUCH] month oder year ist NULL -> return');
return self::SUCCESS;
}
$points = (int) abs($usv->getRawOriginal('points') ?? 0);
if ($points <= 0) {
$this->error("[ABBRUCH] points = {$points} (<= 0) -> return");
return self::SUCCESS;
}
$this->info(" Effektive Punkte: {$points}");
// 3. Aktive Incentives
$this->newLine();
$this->info('[2] Aktive Incentives:');
$active_incentives = Incentive::query()->active()->get();
$this->info(" Anzahl aktive: {$active_incentives->count()}");
foreach ($active_incentives as $incentive) {
$in_scope = $incentive->isDateInScope($month, $year);
$scope_label = $in_scope ? 'IM SCOPE' : 'AUSSERHALB';
$this->info(" #{$incentive->id} {$incentive->name}: {$month}/{$year} -> {$scope_label}");
$this->info(" Qualification: {$incentive->qualification_start} - {$incentive->qualification_end}, Calc End: {$incentive->calculation_end}");
}
// ===== TEIL A: Neupartner-Check =====
$this->newLine();
$this->info('========================================');
$this->info('[A] NEUPARTNER-CHECK: Ist User #'.$usv->user_id.' ein gettrackter Neupartner?');
$this->info('========================================');
$partner_trackings = IncentiveNewPartner::where('user_id', $usv->user_id)
->with('participant.incentive')
->get();
$this->info(" IncentiveNewPartner-Eintraege fuer user_id={$usv->user_id}: {$partner_trackings->count()}");
if ($partner_trackings->isEmpty()) {
$this->warn(' -> User ist KEIN gettrackter Neupartner in irgendeinem Incentive.');
}
foreach ($partner_trackings as $tracking) {
$participant = $tracking->participant;
$incentive = $participant->incentive ?? null;
$this->newLine();
$this->table(['Feld', 'Wert'], [
['NewPartner #', $tracking->id],
['participant_id', $tracking->participant_id],
['Participant User', $participant->user_id],
['Incentive', $incentive ? "#{$incentive->id}: {$incentive->name}" : 'NULL'],
['Incentive Status', $incentive ? $incentive->status : 'NULL'],
['Incentive aktiv?', $incentive && $incentive->status == 1 ? 'JA' : 'NEIN'],
]);
if (! $incentive || $incentive->status != 1) {
$this->warn(' -> Incentive nicht aktiv -> SKIP');
continue;
}
$in_scope = $incentive->isDateInScope($month, $year);
$this->info(" isDateInScope({$month}, {$year}): ".($in_scope ? 'JA' : 'NEIN'));
if (! $in_scope) {
$this->warn(' -> Monat/Jahr ausserhalb Scope -> SKIP');
continue;
}
// Duplikat-Check
$exists = IncentivePointsLog::where('participant_id', $participant->id)
->where('user_sales_volume_id', $usv->id)
->where('is_storno', false)
->exists();
$this->info(' Log-Eintrag existiert bereits: '.($exists ? 'JA (Duplikat -> kein neuer Eintrag)' : 'NEIN -> wuerde erstellt'));
$this->info(' ==> MATCH! Punkte wuerden Participant #'.$participant->id." (User #{$participant->user_id}) gutgeschrieben");
$this->info(" Typ: partner, Punkte: {$points} (accumulated)");
}
// ===== TEIL B: Neuabo-Check =====
$this->newLine();
$this->info('========================================');
$this->info('[B] NEUABO-CHECK: Stammt die Bestellung von einem gettrackten Abo-Kunden?');
$this->info('========================================');
if (! $usv->shopping_order_id) {
$this->warn(' shopping_order_id ist NULL -> Abo-Check uebersprungen.');
} else {
$order = ShoppingOrder::find($usv->shopping_order_id);
if (! $order) {
$this->error(" ShoppingOrder #{$usv->shopping_order_id} nicht gefunden.");
} else {
$this->table(['Feld', 'Wert'], [
['Order ID', $order->id],
['shopping_user_id', $order->shopping_user_id ?? 'NULL'],
['auth_user_id', $order->auth_user_id ?? 'NULL'],
['member_id', $order->member_id ?? 'NULL'],
['payment_for', $order->payment_for],
['is_abo', $order->is_abo ? 'JA' : 'NEIN'],
]);
if (! $order->shopping_user_id) {
$this->warn(' shopping_user_id ist NULL -> kein Abo-Matching moeglich.');
} else {
$abo_trackings = IncentiveNewAbo::whereHas(
'userAbo',
fn ($q) => $q->where('shopping_user_id', $order->shopping_user_id)
)
->with('participant.incentive', 'userAbo')
->get();
$this->info(" IncentiveNewAbo mit shopping_user_id={$order->shopping_user_id}: {$abo_trackings->count()}");
if ($abo_trackings->isEmpty()) {
$this->warn(' -> Keine gettrackten Abos fuer diesen Kunden.');
}
foreach ($abo_trackings as $tracking) {
$participant = $tracking->participant;
$incentive = $participant->incentive ?? null;
$abo = $tracking->userAbo;
$this->newLine();
$this->table(['Feld', 'Wert'], [
['NewAbo #', $tracking->id],
['user_abo_id', $tracking->user_abo_id],
['Abo shopping_user_id', $abo ? $abo->shopping_user_id : 'NULL'],
['participant_id', $tracking->participant_id],
['Participant User', $participant->user_id],
['Incentive', $incentive ? "#{$incentive->id}: {$incentive->name}" : 'NULL'],
['Incentive aktiv?', $incentive && $incentive->status == 1 ? 'JA' : 'NEIN'],
]);
if (! $incentive || $incentive->status != 1) {
$this->warn(' -> Incentive nicht aktiv -> SKIP');
continue;
}
$in_scope = $incentive->isDateInScope($month, $year);
$this->info(" isDateInScope({$month}, {$year}): ".($in_scope ? 'JA' : 'NEIN'));
if (! $in_scope) {
$this->warn(' -> Monat/Jahr ausserhalb Scope -> SKIP');
continue;
}
$exists = IncentivePointsLog::where('participant_id', $participant->id)
->where('user_sales_volume_id', $usv->id)
->where('is_storno', false)
->exists();
$this->info(' Log-Eintrag existiert bereits: '.($exists ? 'JA (Duplikat)' : 'NEIN -> wuerde erstellt'));
$this->info(' ==> MATCH! Punkte wuerden Participant #'.$participant->id." (User #{$participant->user_id}) gutgeschrieben");
$this->info(" Typ: abo, Punkte: {$points} (accumulated)");
}
}
}
}
// ===== Zusammenfassung =====
$this->newLine();
$this->info('=== ZUSAMMENFASSUNG ===');
$total_partner = $partner_trackings->filter(function ($t) use ($month, $year) {
return $t->participant->incentive
&& $t->participant->incentive->status == 1
&& $t->participant->incentive->isDateInScope($month, $year);
})->count();
$this->info(" Neupartner-Matches: {$total_partner}");
$this->info(' Neuabo-Matches: siehe oben');
if ($total_partner === 0) {
$this->warn(' -> Keine Punkte wuerden vergeben (kein Match).');
}
return self::SUCCESS;
}
}

View file

@ -1,106 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class LogCleanup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'logs:cleanup {--days=30 : Number of days to keep logs}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean up old log files older than specified days';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$days = $this->option('days');
$logPath = storage_path('logs');
if (!File::isDirectory($logPath)) {
$this->error("Log directory not found: {$logPath}");
return 1;
}
$deletedFiles = 0;
$deletedSize = 0;
$cutoffDate = Carbon::now()->subDays($days);
$this->info("Cleaning up log files older than {$days} days (before {$cutoffDate->toDateString()})...");
$files = File::files($logPath);
foreach ($files as $file) {
$filename = $file->getFilename();
// Skip the current laravel.log file
if ($filename === 'laravel.log') {
continue;
}
$lastModified = Carbon::createFromTimestamp($file->getMTime());
if ($lastModified->lt($cutoffDate)) {
$fileSize = $file->getSize();
try {
File::delete($file->getPathname());
$deletedFiles++;
$deletedSize += $fileSize;
$this->line("Deleted: {$filename} (" . $this->formatBytes($fileSize) . ", modified: {$lastModified->toDateString()})");
} catch (\Exception $e) {
$this->error("Failed to delete {$filename}: " . $e->getMessage());
}
}
}
if ($deletedFiles > 0) {
$this->info("\nCleanup complete!");
$this->info("Deleted {$deletedFiles} file(s), freed " . $this->formatBytes($deletedSize));
Log::channel('cleanup')->info("Log cleanup completed", [
'deleted_files' => $deletedFiles,
'freed_space' => $this->formatBytes($deletedSize),
'days' => $days
]);
} else {
$this->info("No old log files found to delete.");
}
return 0;
}
/**
* Format bytes to human readable format
*
* @param int $bytes
* @return string
*/
private function formatBytes($bytes)
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
}

View file

@ -1,419 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class PayoneFailedPaypalReport extends Command
{
protected $signature = 'payone:failed-paypal-report
{--from=2026-04-02 : Start-Datum (YYYY-MM-DD)}
{--to= : End-Datum (YYYY-MM-DD), Standard: heute}
{--output=storage/reports/paypal-failed-report.csv : Ausgabedatei}';
protected $description = 'Erstellt einen Schadenbericht über fehlgeschlagene PayPal-Zahlungen (Error 923)';
public function handle(): int
{
$from = $this->option('from');
$to = $this->option('to') ?: now()->format('Y-m-d');
$outputPath = $this->option('output');
$this->info("Schadenbericht PayPal-Ausfälle: {$from} bis {$to}");
$this->newLine();
$orders = $this->getAffectedOrders($from, $to);
if ($orders->isEmpty()) {
$this->warn('Keine fehlgeschlagenen PayPal-Zahlungen im angegebenen Zeitraum gefunden.');
return self::SUCCESS;
}
$this->displaySummary($orders, $from, $to);
$fullPath = base_path($outputPath);
$dir = dirname($fullPath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
$this->writeCsvReport($fullPath, $orders, $from, $to);
$this->writeTxtReport(str_replace('.csv', '.txt', $fullPath), $orders, $from, $to);
$this->writeEmailLists($dir, $orders);
$this->newLine();
$this->info("CSV-Bericht: {$fullPath}");
$this->info('TXT-Bericht: ' . str_replace('.csv', '.txt', $fullPath));
$this->info("E-Mail Berater: {$dir}/emails-berater.csv");
$this->info("E-Mail Shop-Kunden: {$dir}/emails-shop-kunden.csv");
return self::SUCCESS;
}
private function getAffectedOrders(string $from, string $to): \Illuminate\Support\Collection
{
return DB::table('shopping_orders')
->join('shopping_payments', function ($join) {
$join->on('shopping_payments.shopping_order_id', '=', 'shopping_orders.id')
->where('shopping_payments.clearingtype', '=', 'wlt')
->where('shopping_payments.wallettype', '=', 'PPE');
})
->join('payment_transactions', function ($join) {
$join->on('payment_transactions.shopping_payment_id', '=', 'shopping_payments.id')
->where('payment_transactions.errorcode', '=', 923);
})
->join('shopping_users', 'shopping_users.id', '=', 'shopping_orders.shopping_user_id')
->whereBetween('payment_transactions.created_at', ["{$from} 00:00:00", "{$to} 23:59:59"])
->select(
'shopping_orders.id as order_id',
'shopping_orders.total_shipping',
'shopping_orders.paid',
'shopping_orders.txaction',
'shopping_orders.mode',
'shopping_orders.payment_for',
'shopping_orders.auth_user_id',
'shopping_orders.created_at as order_date',
'shopping_users.billing_email',
'shopping_users.billing_firstname',
'shopping_users.billing_lastname',
'shopping_payments.id as payment_id',
'shopping_payments.reference',
'shopping_payments.amount as amount_cents',
'shopping_payments.currency',
'payment_transactions.id as tx_id',
'payment_transactions.errorcode',
'payment_transactions.errormessage',
'payment_transactions.created_at as error_date',
)
->orderBy('payment_transactions.created_at')
->get();
}
private function displaySummary(\Illuminate\Support\Collection $rows, string $from, string $to): void
{
$uniqueOrders = $rows->unique('order_id');
$paidOrders = $uniqueOrders->where('paid', 1);
$unpaidOrders = $uniqueOrders->where('paid', 0);
$this->table(
['Kennzahl', 'Wert'],
[
['Zeitraum', "{$from} bis {$to}"],
['Fehlgeschlagene Transaktionen (Error 923)', $rows->count()],
['Betroffene Bestellungen (eindeutig)', $uniqueOrders->count()],
['Davon nachträglich bezahlt (andere Zahlungsart)', $paidOrders->count()],
['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()],
['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
]
);
}
private function writeCsvReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
{
$fp = fopen($path, 'w');
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($fp, [
'Fehler-Datum',
'Bestell-Nr',
'Bestell-Datum',
'Transaktions-ID',
'Payment-Referenz',
'Betrag (EUR)',
'Fehlercode',
'Fehlermeldung',
'Modus',
'Nachträglich bezahlt',
'Aktueller Status',
], ';');
$uniqueOrders = $rows->unique('order_id');
$unpaidOrders = $uniqueOrders->where('paid', 0);
foreach ($rows as $row) {
fputcsv($fp, [
$row->error_date,
$row->order_id,
$row->order_date,
$row->tx_id,
$row->reference,
number_format($row->total_shipping, 2, ',', ''),
$row->errorcode,
$row->errormessage,
$row->mode,
$row->paid ? 'Ja' : 'Nein',
$row->txaction,
], ';');
}
fputcsv($fp, [], ';');
fputcsv($fp, ['ZUSAMMENFASSUNG'], ';');
fputcsv($fp, ['Zeitraum', "{$from} bis {$to}"], ';');
fputcsv($fp, ['Fehlgeschlagene Transaktionen', $rows->count()], ';');
fputcsv($fp, ['Betroffene Bestellungen', $uniqueOrders->count()], ';');
fputcsv($fp, ['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()], ';');
fputcsv($fp, ['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fputcsv($fp, ['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fclose($fp);
}
private function writeTxtReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
{
$uniqueOrders = $rows->unique('order_id');
$paidOrders = $uniqueOrders->where('paid', 1);
$unpaidOrders = $uniqueOrders->where('paid', 0);
$lines = [];
$lines[] = '================================================================================';
$lines[] = ' SCHADENBERICHT: Fehlgeschlagene PayPal-Zahlungen (PAYONE Error 923)';
$lines[] = '================================================================================';
$lines[] = '';
$lines[] = "Zeitraum: {$from} bis {$to}";
$lines[] = 'Erstellt am: ' . now()->format('d.m.Y H:i:s');
$lines[] = 'Ursache: PayPal-Kontoverknüpfung bei PAYONE nicht migriert (Vertragsübernahme GmbH)';
$lines[] = 'Portal-ID: 2030693';
$lines[] = 'Merchant-ID: 42504';
$lines[] = 'Sub-Account-ID: 43065';
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' ZUSAMMENFASSUNG';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' Fehlgeschlagene Transaktionen (Error 923): %d', $rows->count());
$lines[] = sprintf(' Betroffene Bestellungen (eindeutig): %d', $uniqueOrders->count());
$lines[] = sprintf(' Davon nachträglich bezahlt (andere Methode): %d', $paidOrders->count());
$lines[] = sprintf(' Nicht bezahlt (offen/verloren): %d', $unpaidOrders->count());
$lines[] = '';
$lines[] = sprintf(' Summe nicht bezahlter Bestellungen: %s EUR', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = sprintf(' Summe nachträglich bezahlter Bestellungen: %s EUR', number_format($paidOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = sprintf(' Summe ALLER betroffenen Bestellungen: %s EUR', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' AUFSCHLÜSSELUNG NACH TAG';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$byDate = $rows->groupBy(fn($r) => substr($r->error_date, 0, 10));
foreach ($byDate as $date => $dayRows) {
$dayOrders = $dayRows->unique('order_id');
$dayUnpaid = $dayOrders->where('paid', 0);
$lines[] = sprintf(
' %s: %3d Fehler | %3d Bestellungen | %3d nicht bezahlt | %s EUR offen',
$date,
$dayRows->count(),
$dayOrders->count(),
$dayUnpaid->count(),
number_format($dayUnpaid->sum('total_shipping'), 2, ',', '.')
);
}
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' NICHT BEZAHLTE BESTELLUNGEN (DETAIL)';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(
' %-12s %-20s %-18s %-14s %s',
'Bestell-Nr',
'Datum',
'Referenz',
'Betrag (EUR)',
'Status'
);
$lines[] = ' ' . str_repeat('-', 80);
foreach ($unpaidOrders->sortBy('order_date') as $order) {
$lines[] = sprintf(
' %-12s %-20s %-18s %14s %s',
$order->order_id,
$order->order_date,
$order->reference,
number_format($order->total_shipping, 2, ',', '.'),
$order->txaction
);
}
$lines[] = ' ' . str_repeat('-', 80);
$lines[] = sprintf(
' %-12s %-20s %-18s %14s',
'GESAMT',
'',
$unpaidOrders->count() . ' Bestellungen',
number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.')
);
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' NACHTRÄGLICH BEZAHLTE BESTELLUNGEN (andere Zahlungsart)';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
if ($paidOrders->isEmpty()) {
$lines[] = ' Keine.';
} else {
$lines[] = sprintf(
' %-12s %-20s %-18s %-14s %s',
'Bestell-Nr',
'Datum',
'Referenz',
'Betrag (EUR)',
'Status'
);
$lines[] = ' ' . str_repeat('-', 80);
foreach ($paidOrders->sortBy('order_date') as $order) {
$lines[] = sprintf(
' %-12s %-20s %-18s %14s %s',
$order->order_id,
$order->order_date,
$order->reference,
number_format($order->total_shipping, 2, ',', '.'),
$order->txaction
);
}
$lines[] = ' ' . str_repeat('-', 80);
$lines[] = sprintf(
' %-12s %-20s %-18s %14s',
'GESAMT',
'',
$paidOrders->count() . ' Bestellungen',
number_format($paidOrders->sum('total_shipping'), 2, ',', '.')
);
}
$lines[] = '';
$this->appendEmailSectionToTxt($lines, $unpaidOrders);
$lines[] = '';
$lines[] = '================================================================================';
$lines[] = ' Ende des Berichts';
$lines[] = '================================================================================';
$lines[] = '';
file_put_contents($path, implode("\n", $lines));
}
private function appendEmailSectionToTxt(array &$lines, \Illuminate\Support\Collection $unpaidOrders): void
{
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('billing_email');
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('billing_email');
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' BETROFFENE BERATER (mit Auth-User-ID) - nicht bezahlt';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'Bestell-Nr', 'User-ID', 'Name', 'E-Mail', 'Betrag (EUR)');
$lines[] = ' ' . str_repeat('-', 100);
$beraterSum = 0;
foreach ($berater as $order) {
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
$lines[] = sprintf(
' %-12s %-8s %-30s %-30s %14s',
$order->order_id,
$order->auth_user_id,
mb_substr($name, 0, 28),
mb_substr($order->billing_email ?? '-', 0, 28),
number_format($order->total_shipping, 2, ',', '.')
);
$beraterSum += $order->total_shipping;
}
$lines[] = ' ' . str_repeat('-', 100);
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'GESAMT', '', $berater->count() . ' Bestellungen', '', number_format($beraterSum, 2, ',', '.'));
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' BETROFFENE SHOP-KUNDEN (ohne Auth-User-ID) - nicht bezahlt';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'Bestell-Nr', 'Name', 'E-Mail', 'Betrag (EUR)');
$lines[] = ' ' . str_repeat('-', 90);
$shopSum = 0;
foreach ($shopKunden as $order) {
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
$lines[] = sprintf(
' %-12s %-30s %-30s %14s',
$order->order_id,
mb_substr($name, 0, 28),
mb_substr($order->billing_email ?? '-', 0, 28),
number_format($order->total_shipping, 2, ',', '.')
);
$shopSum += $order->total_shipping;
}
$lines[] = ' ' . str_repeat('-', 90);
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'GESAMT', $shopKunden->count() . ' Bestellungen', '', number_format($shopSum, 2, ',', '.'));
}
private function writeEmailLists(string $dir, \Illuminate\Support\Collection $rows): void
{
$unpaidOrders = $rows->unique('order_id')->where('paid', 0);
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('order_date');
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('order_date');
$this->writeEmailCsv("{$dir}/emails-berater.csv", $berater, true);
$this->writeEmailCsv("{$dir}/emails-shop-kunden.csv", $shopKunden, false);
$this->newLine();
$this->table(
['Kategorie', 'Bestellungen', 'Eindeutige E-Mails', 'Summe (EUR)'],
[
[
'Berater (mit Auth-User-ID)',
$berater->count(),
$berater->pluck('billing_email')->filter()->unique()->count(),
number_format($berater->sum('total_shipping'), 2, ',', '.') . ' EUR',
],
[
'Shop-Kunden (ohne Auth-User-ID)',
$shopKunden->count(),
$shopKunden->pluck('billing_email')->filter()->unique()->count(),
number_format($shopKunden->sum('total_shipping'), 2, ',', '.') . ' EUR',
],
]
);
}
private function writeEmailCsv(string $path, \Illuminate\Support\Collection $orders, bool $includeUserId): void
{
$fp = fopen($path, 'w');
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
$headers = ['Bestell-Nr', 'Bestell-Datum', 'Vorname', 'Nachname', 'E-Mail', 'Betrag (EUR)', 'Status'];
if ($includeUserId) {
array_splice($headers, 1, 0, 'Auth-User-ID');
}
fputcsv($fp, $headers, ';');
foreach ($orders as $order) {
$row = [
$order->order_id,
$order->order_date,
$order->billing_firstname ?? '',
$order->billing_lastname ?? '',
$order->billing_email ?? '',
number_format($order->total_shipping, 2, ',', ''),
$order->txaction,
];
if ($includeUserId) {
array_splice($row, 1, 0, $order->auth_user_id);
}
fputcsv($fp, $row, ';');
}
fputcsv($fp, [], ';');
fputcsv($fp, ['GESAMT', '', '', '', $orders->count() . ' Bestellungen', number_format($orders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fputcsv($fp, ['Eindeutige E-Mail-Adressen', $orders->pluck('billing_email')->filter()->unique()->count()], ';');
fclose($fp);
}
}

View file

@ -1,220 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
use App\Services\AboHelper;
use App\Services\Incentive\IncentiveTracker;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RepairMissingAboFromOrders extends Command
{
protected $signature = 'abo:repair-missing
{--fix : Reparatur ausfuehren (ohne: nur Abgleich/Vorschau)}
{--force : Mit --fix: ohne Rueckfrage (Skripte/CI)}
{--since= : Nur Bestellungen mit created_at >= (Y-m-d)}
{--until= : Nur Bestellungen mit created_at <= Ende dieses Tages (Y-m-d)}
{--order= : Komma-getrennte shopping_order IDs (Filter)}
{--mode=live : Modus: live, test, dev oder all}
{--stats : Zusaetzliche Statistik: bezahlte Abo-Bestellungen vs. mit/ohne UserAboOrder}';
protected $description = 'Abgleich und Reparatur: bezahlte Abo-Bestellungen (Checkout) ohne Verknuepfung user_abo_orders — z. B. nach Payone-Callback vor Erfolgs-Redirect';
public function handle(): int
{
$missing = $this->queryMissingOrders()->orderBy('id')->get();
$this->info('Abgleich: Bestellungen mit is_abo, abo_interval>0, als bezahlt markiert, ohne user_abo_orders-Eintrag.');
$this->newLine();
if ($this->option('stats')) {
$this->printStats();
$this->newLine();
}
$this->info('Treffer (fehlende Verknuepfung): '.$missing->count());
if ($missing->isEmpty()) {
$this->info('Keine Diskrepanz — nichts zu tun.');
return self::SUCCESS;
}
$this->table(
['ID', 'shopping_user_id', 'mode', 'txaction', 'paid', 'created_at'],
$missing->take(200)->map(fn (ShoppingOrder $o) => [
$o->id,
$o->shopping_user_id,
$o->mode,
$o->txaction,
$o->paid ? '1' : '0',
$o->created_at?->format('Y-m-d H:i'),
])
);
if ($missing->count() > 200) {
$this->warn('… und weitere '.($missing->count() - 200).' Eintraege (Ausgabe gekuerzt).');
}
if (! $this->option('fix')) {
$this->newLine();
$this->warn('Trockenlauf. Nutze --fix zur Reparatur (mit Bestaetigung).');
return self::SUCCESS;
}
if (! $this->option('force') && ! $this->confirm('Wirklich '.$missing->count().' Bestellung(en) reparieren?')) {
return self::SUCCESS;
}
$ok = 0;
$fail = 0;
$bar = $this->output->createProgressBar($missing->count());
$bar->start();
foreach ($missing as $order) {
try {
DB::transaction(function () use ($order) {
$this->repairSingleOrder($order);
});
$ok++;
} catch (\Throwable $e) {
$fail++;
$this->newLine();
$this->error("Order #{$order->id}: {$e->getMessage()}");
}
$bar->advance();
}
$bar->finish();
$this->newLine(2);
$this->info("Fertig: {$ok} repariert, {$fail} Fehler.");
return $fail > 0 ? self::FAILURE : self::SUCCESS;
}
/**
* @return \Illuminate\Database\Eloquent\Builder<ShoppingOrder>
*/
private function queryMissingOrders(): \Illuminate\Database\Eloquent\Builder
{
$q = ShoppingOrder::query()
->where('is_abo', true)
->where('abo_interval', '>', 0)
->where(function ($sub) {
$sub->where('paid', true)
->orWhere('paid', 1);
})
->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid'])
->whereNotNull('shopping_user_id')
->whereHas('shopping_payments')
->whereNotExists(function ($sub) {
$sub->select(DB::raw('1'))
->from('user_abo_orders')
->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id');
});
if ($ids = $this->parseOrderIds()) {
$q->whereIn('id', $ids);
}
if ($since = $this->option('since')) {
$q->where('created_at', '>=', $since.' 00:00:00');
}
if ($until = $this->option('until')) {
$q->where('created_at', '<=', $until.' 23:59:59');
}
$mode = (string) $this->option('mode');
if ($mode !== 'all') {
$q->where('mode', $mode);
}
return $q;
}
/**
* @return list<int>
*/
private function parseOrderIds(): array
{
$raw = $this->option('order');
if ($raw === null || $raw === '') {
return [];
}
return array_values(array_filter(array_map('intval', explode(',', (string) $raw))));
}
private function printStats(): void
{
$mode = (string) $this->option('mode');
$base = ShoppingOrder::query()
->where('is_abo', true)
->where('abo_interval', '>', 0)
->where(function ($sub) {
$sub->where('paid', true)->orWhere('paid', 1);
})
->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid']);
if ($since = $this->option('since')) {
$base->where('created_at', '>=', $since.' 00:00:00');
}
if ($until = $this->option('until')) {
$base->where('created_at', '<=', $until.' 23:59:59');
}
if ($mode !== 'all') {
$base->where('mode', $mode);
}
if ($ids = $this->parseOrderIds()) {
$base->whereIn('id', $ids);
}
$totalPaidAbo = (clone $base)->count();
$withLink = (clone $base)->whereExists(function ($sub) {
$sub->select(DB::raw('1'))
->from('user_abo_orders')
->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id');
})->count();
$this->table(
['Kennzahl', 'Anzahl'],
[
['Bezahlte Abo-Bestellungen (Filter)', $totalPaidAbo],
['Davon mit user_abo_orders', $withLink],
['Davon ohne user_abo_orders', max(0, $totalPaidAbo - $withLink)],
]
);
}
private function repairSingleOrder(ShoppingOrder $order): void
{
$payment = ShoppingPayment::query()
->where('shopping_order_id', $order->id)
->orderByDesc('id')
->first();
if (! $payment) {
throw new \RuntimeException('Kein ShoppingPayment zur Bestellung.');
}
$order->loadMissing(['shopping_user', 'shopping_order_items']);
$payment->loadMissing(['payment_transactions']);
$payment->setRelation('shopping_order', $order);
AboHelper::createNewAbo($payment);
$order->refresh();
if (! $order->getUserAbo()) {
throw new \RuntimeException('createNewAbo hat kein UserAbo erzeugt (pruefen: abo_interval, Bestellpositionen, ShoppingPayment.abo_interval).');
}
AboHelper::setAboActive($order, 2, true);
IncentiveTracker::trackAboActivated($order);
}
}

View file

@ -1,129 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\ShoppingOrder;
use App\Repositories\InvoiceRepository;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\Incentive\IncentiveTracker;
use Illuminate\Console\Command;
class RepairMissingInvoices extends Command
{
protected $signature = 'repair:missing-invoices
{--fix : Tatsaechlich reparieren (ohne Flag nur Vorschau)}
{--no-mail : Keine Rechnungs-Mails versenden}
{--since=2026-02-20 : Ab welchem Datum suchen}';
protected $description = 'Repariert fehlende Rechnungen und SalesVolumes fuer bezahlte Bestellungen (Bug: addSalesPointsVolumeUser)';
public function handle(): int
{
$since = $this->option('since') ?? '2026-03-16';
$fix = $this->option('fix') ?? false;
$orders = ShoppingOrder::query()
->where('mode', 'live')
->where('paid', 0)
->where('txaction', 'paid')
->where('created_at', '>=', $since)
->whereNull('deleted_at')
->whereDoesntHave('user_invoice')
->whereDoesntHave('user_sales_volume')
// ->whereDoesntHave('shopping_payments', fn($q) => $q->where('clearingtype', 'vor'))
->orderBy('created_at')
->get();
$this->info("Betroffene Bestellungen seit {$since}: {$orders->count()}");
if ($orders->isEmpty()) {
$this->info('Keine betroffenen Bestellungen gefunden.');
return self::SUCCESS;
}
// Zusammenfassung
$total = $orders->sum('total');
$byPaymentFor = $orders->groupBy('payment_for')->map->count();
$this->table(
['payment_for', 'Anzahl'],
$byPaymentFor->map(fn ($count, $type) => [$type, $count])->values()
);
$this->info("Gesamtwert: {$total} EUR");
if (! $fix) {
$this->warn('Trockenlauf! Nutze --fix um die Reparatur durchzufuehren.');
$this->newLine();
// Erste 10 anzeigen
$this->table(
['ID', 'payment_for', 'total', 'txaction', 'created_at'],
$orders->take(100)->map(fn ($o) => [
$o->id,
$o->payment_for,
$o->total,
$o->txaction,
$o->created_at->format('Y-m-d H:i'),
])
);
if ($orders->count() > 100) {
$this->info('... und '.($orders->count() - 100).' weitere');
}
return self::SUCCESS;
}
$send_mail = ! $this->option('no-mail');
if ($send_mail) {
$this->info('Rechnungs-Mails werden versendet. Nutze --no-mail um dies zu unterdruecken.');
} else {
$this->warn('Rechnungs-Mails werden NICHT versendet.');
}
if (! $this->confirm("Wirklich {$orders->count()} Bestellungen reparieren?")) {
return self::SUCCESS;
}
$success = 0;
$errors = 0;
$bar = $this->output->createProgressBar($orders->count());
$bar->start();
foreach ($orders as $order) {
try {
// 1. SalesVolume erstellen
$user_sales_volume = SalesPointsVolume::User($order);
// 2. Rechnung erstellen (mit Mail-Versand)
$invoice_repo = new InvoiceRepository($order);
$user_invoice = $invoice_repo->create([
'invoice_send_mail' => $send_mail,
]);
// 3. SalesVolume mit Rechnung verknuepfen
$user_sales_volume->user_invoice_id = $user_invoice->id;
$user_sales_volume->save();
// 4. Incentive tracking (falls relevant)
IncentiveTracker::trackSalesVolume($user_sales_volume);
$success++;
$this->info("Order #{$order->id}: Reparatur erfolgreich");
} catch (\Throwable $e) {
$errors++;
$this->newLine();
$this->error("Order #{$order->id}: {$e->getMessage()}");
}
$bar->advance();
}
$bar->finish();
$this->newLine(2);
$this->info("Fertig: {$success} repariert, {$errors} Fehler.");
return $errors > 0 ? self::FAILURE : self::SUCCESS;
}
}

View file

@ -1,297 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\AboHelper;
use App\Services\Incentive\IncentiveTracker;
use App\Services\MyLog;
use App\Services\Payment;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RetryFailedPaypalAbos extends Command
{
protected $signature = 'abo:retry-failed-paypal
{--dry-run : Nur anzeigen, keine Bestellungen ausführen}
{--abo-id= : Nur ein bestimmtes Abo erneut ausführen}';
protected $description = 'Führt Abo-Bestellungen erneut aus, die aufgrund der PayPal-Panne (Error 923) fehlgeschlagen sind';
private float $timeStart;
public function handle(): int
{
$this->timeStart = microtime(true);
$dryRun = $this->option('dry-run');
$singleAboId = $this->option('abo-id');
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Gestartet', [
'dry_run' => $dryRun,
'abo_id' => $singleAboId,
]);
$this->info($dryRun ? '=== DRY-RUN Modus (keine Bestellungen) ===' : '=== LIVE Modus ===');
$this->newLine();
$abos = $this->getAffectedAbos($singleAboId);
if ($abos->isEmpty()) {
$this->warn('Keine betroffenen PayPal-Abos gefunden.');
return self::SUCCESS;
}
$this->displayAboList($abos);
if (! $dryRun && ! $singleAboId) {
if (! $this->confirm("Sollen alle {$abos->count()} Abos jetzt erneut ausgeführt werden?")) {
$this->info('Abgebrochen.');
return self::SUCCESS;
}
}
$results = ['success' => 0, 'error' => 0, 'skipped' => 0];
foreach ($abos as $userAbo) {
if ($dryRun) {
$this->info(" [DRY-RUN] Abo #{$userAbo->id} würde ausgeführt werden");
$results['skipped']++;
continue;
}
try {
$result = $this->retryAboOrder($userAbo);
if ($result) {
$results['success']++;
} else {
$results['error']++;
}
} catch (\Throwable $e) {
$results['error']++;
\Log::channel('abo_order')->error('RetryFailedPaypalAbos: Exception', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
]);
$this->error(" Abo #{$userAbo->id}: Exception - {$e->getMessage()}");
}
}
$this->newLine();
$this->table(
['Ergebnis', 'Anzahl'],
[
['Erfolgreich', $results['success']],
['Fehlgeschlagen', $results['error']],
['Übersprungen (Dry-Run)', $results['skipped']],
]
);
$executionTime = $this->getExecutionTime();
$this->info("Abgeschlossen in {$executionTime}");
\Log::channel('abo_order')->info("RetryFailedPaypalAbos: Abgeschlossen in {$executionTime}", $results);
return self::SUCCESS;
}
/**
* @return \Illuminate\Database\Eloquent\Collection<int, UserAbo>
*/
private function getAffectedAbos(?string $singleAboId): \Illuminate\Database\Eloquent\Collection
{
$query = UserAbo::query()
->where('status', 3)
->where('active', true)
->where('clearingtype', 'wlt')
->where('wallettype', 'PPE')
->whereRaw("DATE(next_date) = '2026-04-05'")
->with(['shopping_user', 'user_abo_items']);
if ($singleAboId) {
$query->where('id', $singleAboId);
}
return $query->orderBy('id')->get();
}
private function displayAboList(\Illuminate\Database\Eloquent\Collection $abos): void
{
$rows = $abos->map(fn (UserAbo $abo) => [
$abo->id,
$abo->user_id ?? '-',
$abo->is_for,
$abo->email,
$abo->abo_interval,
$abo->getRawOriginal('next_date'),
$abo->user_abo_items->count().' Artikel',
]);
$this->table(
['Abo-ID', 'User-ID', 'Typ', 'E-Mail', 'Intervall', 'Next-Date', 'Artikel'],
$rows->toArray()
);
$this->info("Betroffene Abos: {$abos->count()}");
$this->newLine();
}
private function retryAboOrder(UserAbo $userAbo): bool
{
$this->info(" Verarbeite Abo #{$userAbo->id} ({$userAbo->email})...");
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Verarbeite Abo', [
'abo_id' => $userAbo->id,
'email' => $userAbo->email,
'payone_userid' => $userAbo->payone_userid,
]);
$alreadyPaidToday = UserAboOrder::where('user_abo_id', $userAbo->id)
->whereDate('created_at', now()->toDateString())
->where('paid', true)
->exists();
if ($alreadyPaidToday) {
$this->warn(" Abo #{$userAbo->id}: Bereits heute bezahlt - übersprungen");
return true;
}
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
$shoppingOrder = null;
$userOrder = new UserMakeOrder($userAbo);
try {
if (! $userOrder->createShoppingUser()) {
$this->error(" Abo #{$userAbo->id}: Shopping-User konnte nicht erstellt werden");
return false;
}
$shoppingOrder = $userOrder->makeShoppingOrder();
if (! $shoppingOrder) {
$this->error(" Abo #{$userAbo->id}: Bestellung konnte nicht erstellt werden");
return false;
}
$this->info(" Bestellung #{$shoppingOrder->id} erstellt (Betrag: {$shoppingOrder->total_shipping} EUR)");
$response = $userOrder->makePayment();
if (is_object($response)) {
$response = (array) $response;
}
if (! isset($response['status'])) {
$this->error(" Abo #{$userAbo->id}: Ungültige Zahlungsantwort");
$this->markAboError($userAbo, $shoppingOrder);
return false;
}
if ($response['status'] === 'APPROVED') {
$this->info(" Zahlung ERFOLGREICH für Abo #{$userAbo->id}");
$this->markAboSuccess($userAbo, $shoppingOrder);
return true;
}
$errorCode = $response['errorcode'] ?? '-';
$errorMsg = $response['errormessage'] ?? '-';
$this->error(" Zahlung FEHLGESCHLAGEN für Abo #{$userAbo->id}: [{$errorCode}] {$errorMsg}");
MyLog::writeLog(
'userabo',
'error',
'Error:RetryPaypal RetryFailedPaypalAbos / makePayment Error',
$response
);
$this->markAboError($userAbo, $shoppingOrder);
$shoppingPayment = $userOrder->getShoppingPayment();
if ($shoppingPayment) {
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, [
'mode' => $shoppingPayment->mode,
'txaction' => 'error',
'send_link' => false,
'payment_error' => $response,
]);
}
return false;
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('RetryFailedPaypalAbos: Exception bei Abo-Verarbeitung', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error(" Exception: {$e->getMessage()}");
if ($shoppingOrder) {
$this->markAboError($userAbo, $shoppingOrder);
}
return false;
}
}
private function markAboSuccess(UserAbo $userAbo, $shoppingOrder): void
{
DB::transaction(function () use ($userAbo, $shoppingOrder) {
$nextDate = AboHelper::setNextDate(now(), $userAbo->abo_interval);
$userAbo->update([
'status' => 2,
'next_date' => $nextDate,
'last_date' => now(),
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 1,
'paid' => true,
]);
});
IncentiveTracker::trackAboActivated($shoppingOrder);
$nextDateFormatted = Carbon::parse($userAbo->getRawOriginal('next_date'))->format('d.m.Y');
$this->info(" Status → 2 (abo_okay), nächstes Datum → {$nextDateFormatted}");
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Abo erfolgreich reaktiviert', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'next_date' => $userAbo->getRawOriginal('next_date'),
]);
}
private function markAboError(UserAbo $userAbo, $shoppingOrder): void
{
DB::transaction(function () use ($userAbo, $shoppingOrder) {
$userAbo->update(['last_date' => now()]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 3,
'paid' => false,
]);
});
}
private function getExecutionTime(): string
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
return $sec.' Sekunden und '.round($micro * 1000, 2).' ms';
}
}

View file

@ -1,664 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Http\Controllers\Api\KasController;
use App\Http\Controllers\Api\KasSLLController;
use App\Models\UserShop;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SubDomains extends Command
{
/**
* Die Signatur des Konsolenbefehls.
*
* Aufruf: php artisan subdomains:action --force --start=4 --debug
* /usr/bin/php82 artisan subdomains:action --force --start=4 --create-missing --debug
* /usr/bin/php82 artisan subdomains:action --force --debug
* @var string
*/
protected $signature = 'subdomains:action {user_id?} {--force} {--start=1} {--create-missing} {--debug}';
/**
* Die Beschreibung des Konsolenbefehls.
*
* @var string
*/
protected $description = 'Passt Parameter für die User-Subdomains an (PHP-Version, SSL)';
/**
* Zeitstempel für die Ausführungszeit-Messung
*
* @var float
*/
private $timeStart;
/**
* Standard-Domain für alle Subdomains
*
* @var string
*/
private $domain = 'mivita.care';
/**
* Zu überspringende Subdomain-Präfixe
*
* @var array
*/
private $skipPrefixes = ['www.', 'api.', 'checkout.', 'preview.'];
/**
* Erstellt eine neue Befehlsinstanz.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Führt den Konsolenbefehl aus.
*
* @return int
*/
public function handle()
{
$this->timeStart = microtime(true);
$userId = $this->argument('user_id');
$force = $this->option('force');
$startId = $this->option('start');
$createMissing = $this->option('create-missing');
$debug = $this->option('debug');
if ($debug) {
$this->warn("DEBUG-MODUS (DRY-RUN): Es werden keine tatsächlichen Änderungen vorgenommen!");
}
$this->info("Starte Subdomain-Verwaltung" . ($force ? " (erzwungener Modus)" : ""));
try {
if ($userId) {
$this->info("Verarbeite einzelnen Benutzer mit ID: {$userId}");
$result = $this->syncSingleUser($userId, $force, $createMissing, $debug);
if ($result) {
$this->info("Benutzer {$userId} erfolgreich synchronisiert" . ($debug ? " (simuliert)" : ""));
} else {
$this->warn("Benutzer {$userId} konnte nicht vollständig synchronisiert werden" . ($debug ? " (simuliert)" : ""));
}
} else {
$this->info("Verarbeite alle Benutzer ab ID: {$startId}");
$result = $this->syncAllUsers($force, $startId, $createMissing, $debug);
// Zusammenfassung der Ergebnisse
$this->displaySummary($result, $debug);
}
$this->logExecutionTime("Subdomain-Verwaltung abgeschlossen" . ($debug ? " (DEBUG-MODUS)" : ""));
return 0;
} catch (Exception $e) {
$this->error("Ein Fehler ist aufgetreten: " . $e->getMessage());
Log::error("Shopping User Sync Fehler: ", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => $userId,
'force' => $force,
'start_id' => $startId,
'debug' => $debug
]);
return 1;
}
}
/**
* Zeigt eine Zusammenfassung der Synchronisationsergebnisse an
*
* @param array $result Ergebnisdaten der Synchronisation
* @param bool $debug Debug-Modus (Dry-Run)
* @return void
*/
private function displaySummary($result, $debug = false)
{
$this->line("");
$this->info("=== Zusammenfassung " . ($debug ? "(DEBUG-MODUS)" : "") . " ===");
$this->info("Verarbeitete Shops: " . count($result['shops']));
$this->info("Aktualisierte PHP-Versionen: " . $result['updatedCount'] . ($debug ? " (simuliert)" : ""));
$this->info("Aktivierte SSL-Zertifikate: " . $result['sslEnabledCount'] . ($debug ? " (simuliert)" : ""));
$this->info("Aktualisierte SSL-Konfigurationen: " . $result['sslConfiguredCount'] . ($debug ? " (simuliert)" : ""));
if (!empty($result['createdSubdomains'])) {
$this->info("Neu erstellte Subdomains: " . count($result['createdSubdomains']) . ($debug ? " (simuliert)" : ""));
}
if (!empty($result['missingSubdomains'])) {
$this->warn("Fehlende Subdomains: " . count($result['missingSubdomains']));
}
if (!empty($result['unusedSubdomains'])) {
$this->warn("Ungenutzte Subdomains: " . count($result['unusedSubdomains']));
}
if (!empty($result['doubleDomains'])) {
$this->warn("Benutzer mit mehreren Shops: " . count($result['doubleDomains']));
}
}
/**
* Synchronisiert einen einzelnen Benutzer und seine Shops
*
* @param int $userId Benutzer-ID
* @param bool $force Erzwingt die Aktualisierung aller Subdomains
* @param bool $createMissing Erstellt fehlende Subdomains
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function syncSingleUser($userId, $force = false, $createMissing = false, $debug = false)
{
$this->info("Synchronisiere Benutzer mit ID: {$userId}");
// Benutzer-Shops abrufen
$userShops = UserShop::where('user_id', $userId)->get();
if ($userShops->isEmpty()) {
$this->warn("Keine Shops für Benutzer {$userId} gefunden");
return false;
}
$this->info("Gefundene Shops für Benutzer {$userId}: " . $userShops->count());
// Subdomains abrufen und filtern
$subdomains = $this->getFilteredSubdomains();
$success = true;
// Benutzer-Shops durchlaufen und mit Subdomains abgleichen
foreach ($userShops as $userShop) {
$fullDomainName = $userShop->slug . '.' . $this->domain;
$this->info("Verarbeite Shop: {$fullDomainName}");
// Prüfen, ob Subdomain existiert
if (array_key_exists($fullDomainName, $subdomains)) {
$success = $this->processExistingSubdomain($userShop, $subdomains[$fullDomainName], $force, $debug) && $success;
} else {
// Subdomain fehlt
$this->warn("Shop {$userShop->slug}: Keine Subdomain gefunden");
// Optional: Neue Subdomain erstellen
if ($createMissing) {
$this->info("Erstelle fehlende Subdomain für Shop {$userShop->slug}" . ($debug ? " (simuliert)" : ""));
$success = $this->createSubdomain($userShop->slug, $debug) && $success;
} else {
$success = false;
}
}
}
return $success;
}
/**
* Verarbeitet eine existierende Subdomain und aktualisiert sie bei Bedarf
*
* @param UserShop $userShop Shop-Objekt
* @param array $subdomainInfo Subdomain-Informationen
* @param bool $force Erzwingt die Aktualisierung
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function processExistingSubdomain($userShop, $subdomainInfo, $force, $debug = false)
{
$success = true;
$hasSSL = ($subdomainInfo['ssl_certificate_sni'] === 'Y' ||
$subdomainInfo['ssl_proxy'] === 'Y');
$sslActive = ($subdomainInfo['ssl_certificate_sni_is_active'] ?? 'N') === 'Y';
$phpVersion = $subdomainInfo['php_version'];
$this->info("Shop {$userShop->slug}: PHP-Version: {$phpVersion}, SSL: " .
($hasSSL ? "Aktiviert" : "Nicht aktiviert") .
($hasSSL ? ", SSL aktiv: " . ($sslActive ? "Ja" : "Nein") : ""));
// Prüfen, ob PHP-Version aktualisiert werden muss
$requiredPhpVersion = config('app.php_version');
if ($force || $phpVersion !== $requiredPhpVersion) {
$this->info("Shop {$userShop->slug}: PHP-Version aktualisieren von {$phpVersion} auf {$requiredPhpVersion}" . ($debug ? " (simuliert)" : ""));
if (!$debug && !$this->updateSubdomainParams($userShop->slug, $requiredPhpVersion)) {
$this->error("PHP-Version für {$userShop->slug}.{$this->domain} konnte nicht aktualisiert werden");
$success = false;
}
}
// Prüfen, ob SSL aktiviert werden muss
if ($force || !$hasSSL) {
$this->info("Shop {$userShop->slug}: SSL aktivieren" . ($debug ? " (simuliert)" : ""));
if (!$debug && !$this->enableSSL($userShop->slug)) {
$this->error("SSL für {$userShop->slug}.{$this->domain} konnte nicht aktiviert werden");
$success = false;
}
}
// Prüfen, ob SSL-Konfiguration aktualisiert werden muss
else if ($force || ($hasSSL && !$sslActive)) {
$this->info("Shop {$userShop->slug}: SSL-Konfiguration aktualisieren" . ($debug ? " (simuliert)" : ""));
if (!$debug && !$this->updateSSL($userShop->slug . '.' . $this->domain)) {
$this->error("SSL-Konfiguration für {$userShop->slug}.{$this->domain} konnte nicht aktualisiert werden");
$success = false;
}
}
return $success;
}
/**
* Synchronisiert alle Benutzer-Shops
*
* @param bool $force Erzwingt die Aktualisierung aller Subdomains
* @param int $startId Beginnt die Synchronisation ab dieser ID
* @param bool $createMissing Erstellt fehlende Subdomains
* @param bool $debug Debug-Modus (Dry-Run)
* @return array Ergebnisdaten der Synchronisation
*/
private function syncAllUsers($force, $startId, $createMissing = false, $debug = false)
{
$this->info("Starte Synchronisation aller Benutzer-Shops ab ID: {$startId}");
// Benutzer-Shops abrufen
$userShopsQuery = UserShop::query();
if ($startId > 1) {
$userShopsQuery->where('id', '>=', $startId);
}
$userShops = $userShopsQuery->limit(1000)->get();
$this->info("Gefundene Benutzer-Shops: " . $userShops->count());
// Subdomains abrufen und filtern
$subdomains = $this->getFilteredSubdomains();
$this->info("Gefilterte Subdomains: " . count($subdomains));
// Ergebnis-Arrays initialisieren
$doubleDomains = [];
$missingSubdomains = [];
$outdatedPhpVersions = [];
$missingSSL = [];
$sslConfigurationNeeded = [];
$createdSubdomains = [];
$updatedCount = 0;
$sslEnabledCount = 0;
$sslConfiguredCount = 0;
// Benutzer-Shops durchlaufen und mit Subdomains abgleichen
foreach ($userShops as $userShop) {
$fullDomainName = $userShop->slug . '.' . $this->domain;
// Status der Subdomain setzen
$userShop->hasSubdomain = false;
$userShop->hasSSL = false;
$userShop->sslActive = false;
$userShop->PHPversion = "";
// Prüfen, ob Subdomain existiert
if (array_key_exists($fullDomainName, $subdomains)) {
$userShop->hasSubdomain = true;
$userShop->hasSSL = ($subdomains[$fullDomainName]['ssl_certificate_sni'] === 'Y' ||
$subdomains[$fullDomainName]['ssl_proxy'] === 'Y');
$userShop->sslActive = ($subdomains[$fullDomainName]['ssl_certificate_sni_is_active'] ?? 'N') === 'Y';
$userShop->PHPversion = $subdomains[$fullDomainName]['php_version'];
// Prüfen, ob PHP-Version aktualisiert werden muss
$requiredPhpVersion = config('app.php_version');
if ($force || $userShop->PHPversion !== $requiredPhpVersion) {
$this->info("Shop {$userShop->slug}: PHP-Version aktualisieren von {$userShop->PHPversion} auf {$requiredPhpVersion}" . ($debug ? " (simuliert)" : ""));
$outdatedPhpVersions[] = $userShop->slug;
if (!$debug && $this->updateSubdomainParams($userShop->slug, $requiredPhpVersion)) {
$updatedCount++;
} else if ($debug) {
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
$updatedCount++;
}
}else{
$this->info("Shop {$userShop->slug}: PHP-Version ist aktuell: {$userShop->PHPversion}");
}
// Prüfen, ob SSL aktiviert werden muss
/* if ($force || !$userShop->hasSSL) {
$this->info("Shop {$userShop->slug}: SSL aktivieren" . ($debug ? " (simuliert)" : ""));
$missingSSL[] = $userShop->slug;
if (!$debug && $this->enableSSL($userShop->slug)) {
$sslEnabledCount++;
} else if ($debug) {
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
$sslEnabledCount++;
}
}
// Prüfen, ob SSL-Konfiguration aktualisiert werden muss
else if ($force || ($userShop->hasSSL && !$userShop->sslActive)) {
$this->info("Shop {$userShop->slug}: SSL-Konfiguration aktualisieren" . ($debug ? " (simuliert)" : ""));
$sslConfigurationNeeded[] = $userShop->slug;
if (!$debug && $this->updateSSL($fullDomainName)) {
$sslConfiguredCount++;
} else if ($debug) {
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
$sslConfiguredCount++;
}
}*/
// Subdomain aus der Liste entfernen, um später ungenutzte zu identifizieren
unset($subdomains[$fullDomainName]);
} else {
// Subdomain fehlt
$missingSubdomains[] = $userShop->slug;
$this->warn("Shop {$userShop->slug}: Keine Subdomain gefunden");
// Optional: Neue Subdomain erstellen
if ($createMissing) {
$this->info("Erstelle fehlende Subdomain für Shop {$userShop->slug}" . ($debug ? " (simuliert)" : ""));
if (!$debug && $this->createSubdomain($userShop->slug)) {
$createdSubdomains[] = $userShop->slug;
} else if ($debug) {
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
$createdSubdomains[] = $userShop->slug;
}
}
}
// Doppelte Domains pro Benutzer erfassen
$doubleDomains[$userShop->user_id][$userShop->id] = $fullDomainName;
}
// Bereinigen der doppelten Domains (nur Benutzer mit mehreren Shops behalten)
foreach ($doubleDomains as $userId => $domains) {
if (count($domains) === 1) {
unset($doubleDomains[$userId]);
}
}
$this->logExecutionTime("Synchronisation abgeschlossen" . ($debug ? " (DEBUG-MODUS)" : ""));
// Ergebnisdaten zurückgeben
return [
'shops' => $userShops,
'unusedSubdomains' => $subdomains,
'doubleDomains' => $doubleDomains,
'missingSubdomains' => $missingSubdomains,
'outdatedPhpVersions' => $outdatedPhpVersions,
'missingSSL' => $missingSSL,
'sslConfigurationNeeded' => $sslConfigurationNeeded,
'createdSubdomains' => $createdSubdomains,
'updatedCount' => $updatedCount,
'sslEnabledCount' => $sslEnabledCount,
'sslConfiguredCount' => $sslConfiguredCount
];
}
/**
* Ruft alle Subdomains ab und filtert sie
*
* @return array Gefilterte Subdomains
*/
private function getFilteredSubdomains()
{
$kas = new KasController();
$subdomains = [];
// Alle Subdomains abrufen
$this->info("Rufe Subdomains von KAS ab...");
$getSubdomains = $kas->action('get_subdomains');
// Subdomains filtern und in ein leicht zugängliches Array umwandeln
foreach ($getSubdomains as $subdomain) {
if (!isset($subdomain['subdomain_name'])) {
continue;
}
// Spezielle Subdomains überspringen
$skip = false;
foreach ($this->skipPrefixes as $prefix) {
if (strpos($subdomain['subdomain_name'], $prefix) !== false) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
// Subdomain-Informationen speichern
$subdomains[$subdomain['subdomain_name']] = [
'ssl_certificate_sni' => $subdomain['ssl_certificate_sni'] ?? 'N',
'php_version' => $subdomain['php_version'] ?? '',
'ssl_proxy' => $subdomain['ssl_proxy'] ?? 'N',
'ssl_certificate_sni_is_active' => $subdomain['ssl_certificate_sni_is_active'] ?? 'N',
];
}
return $subdomains;
}
/**
* Ändert Parameter einer Subdomain, insbesondere die PHP-Version
*
* @param string $subdomain Name der Subdomain ohne Domain
* @param string $phpVersion Neue PHP-Version (z.B. '8.2')
* @param array $additionalParams Weitere zu ändernde Parameter
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function updateSubdomainParams($subdomain, $phpVersion, $additionalParams = [], $debug = false)
{
$this->info("Aktualisiere Parameter für: {$subdomain}.{$this->domain}" . ($debug ? " (simuliert)" : ""));
if ($debug) {
$this->line(" - PHP-Version: {$phpVersion}");
if (!empty($additionalParams)) {
$this->line(" - Zusätzliche Parameter: " . json_encode($additionalParams));
}
return true;
}
try {
$kas = new KasController();
// Standardparameter
$params = [
'subdomain_name' => $subdomain . '.' . $this->domain, // Vollständigen Domainnamen verwenden
'php_version' => $phpVersion
];
// Oder alternativ, falls die API die Subdomain und Domain getrennt erwartet:
// $params = [
// 'subdomain_name' => $subdomain,
// 'domain_name' => $this->domain,
// 'php_version' => $phpVersion
// ];
// Zusätzliche Parameter hinzufügen
$params = array_merge($params, $additionalParams);
// Subdomain aktualisieren
$result = $kas->action('update_subdomain', $params);
$this->info("Parameter: ".json_encode($params));
if ($result) {
$this->info("Parameter für {$subdomain}.{$this->domain} erfolgreich aktualisiert " . json_encode($result));
return true;
} else {
$this->error("Fehler beim Aktualisieren der Parameter für {$subdomain}.{$this->domain}: " . json_encode($result));
return false;
}
} catch (Exception $e) {
$this->error("Fehler beim Aktualisieren der Parameter für {$subdomain}.{$this->domain}: " . $e->getMessage());
Log::error("Subdomain Parameter Update Fehler", [
'subdomain' => $subdomain,
'domain' => $this->domain,
'php_version' => $phpVersion,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return false;
}
}
/**
* Aktualisiert die SSL-Konfiguration einer Subdomain mit erweiterten Parametern
*
* @param string $subdomainName Vollständiger Domainname (z.B. 'shop.mivita.care')
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function updateSSL($subdomainName, $debug = false)
{
$this->info("Aktualisiere SSL-Konfiguration für: {$subdomainName}" . ($debug ? " (simuliert)" : ""));
if ($debug) {
$this->line(" - SSL-Parameter werden aktualisiert");
return true;
}
try {
$kas = new KasController();
$ssl = KasSLLController::getApiSSLParameter();
$params = array_merge(['hostname' => $subdomainName], $ssl);
$result = $kas->action('update_ssl', $params);
if ($result === "TRUE" || $result === true) {
$this->info("SSL-Konfiguration für {$subdomainName} erfolgreich aktualisiert");
return true;
} else {
$this->warn("SSL-Konfiguration für {$subdomainName} nicht vollständig aktualisiert: " . json_encode($result));
return false;
}
} catch (Exception $e) {
$this->error("Fehler bei der SSL-Konfiguration für {$subdomainName}: " . $e->getMessage());
Log::error("SSL Update Fehler", [
'subdomain' => $subdomainName,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return false;
}
}
/**
* Aktiviert SSL für eine Subdomain mit vollständiger Konfiguration
*
* @param string $subdomain Name der Subdomain ohne Domain
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function enableSSL($subdomain, $debug = false)
{
$fullDomainName = $subdomain . '.' . $this->domain;
$this->info("Aktiviere SSL für: {$fullDomainName}" . ($debug ? " (simuliert)" : ""));
if ($debug) {
$this->line(" - SSL-Proxy wird aktiviert");
$this->line(" - HTTP zu HTTPS Weiterleitung wird eingerichtet");
$this->line(" - SSL-Konfiguration wird aktualisiert");
return true;
}
// Schritt 1: Subdomain-Parameter aktualisieren (ssl_proxy aktivieren)
$subdomainResult = $this->updateSubdomainParams($subdomain, config('app.php_version'), [
'ssl_proxy' => 'Y',
'redirect_status' => 301 // Weiterleitung von HTTP auf HTTPS
]);
if (!$subdomainResult) {
$this->error("SSL-Aktivierung für {$fullDomainName} fehlgeschlagen: Subdomain-Parameter konnten nicht aktualisiert werden");
return false;
}
// Schritt 2: SSL-Konfiguration aktualisieren
$sslResult = $this->updateSSL($fullDomainName);
if (!$sslResult) {
$this->warn("SSL-Aktivierung für {$fullDomainName} teilweise erfolgreich: SSL-Konfiguration konnte nicht aktualisiert werden");
return false;
}
$this->info("SSL für {$fullDomainName} vollständig aktiviert");
return true;
}
/**
* Erstellt eine neue Subdomain für einen Shop
*
* @param string $slug Shop-Slug
* @param bool $debug Debug-Modus (Dry-Run)
* @return bool Erfolg der Operation
*/
private function createSubdomain($slug, $debug = false)
{
$fullDomainName = $slug . '.' . $this->domain;
$this->info("Erstelle neue Subdomain: {$fullDomainName}" . ($debug ? " (simuliert)" : ""));
if ($debug) {
$this->line(" - Pfad: /mein.mivita.care/public/");
$this->line(" - PHP-Version: " . config('app.php_version'));
$this->line(" - SSL wird direkt aktiviert");
return true;
}
try {
$kas = new KasController();
// Parameter für die neue Subdomain
$params = [
'subdomain_name' => $slug,
'domain_name' => $this->domain,
'subdomain_path' => '/mein.mivita.care/public/',
'php_version' => config('app.php_version'),
];
// Subdomain erstellen
$result = $kas->action('add_subdomain', $params);
if ($result === $fullDomainName) {
$this->info("Subdomain {$fullDomainName} erfolgreich erstellt");
// SSL direkt aktivieren
$this->enableSSL($slug);
return true;
} else {
$this->error("Fehler beim Erstellen der Subdomain {$fullDomainName}: " . json_encode($result));
return false;
}
} catch (Exception $e) {
$this->error("Fehler beim Erstellen der Subdomain {$fullDomainName}: " . $e->getMessage());
Log::error("Subdomain Erstellung Fehler", [
'slug' => $slug,
'domain' => $this->domain,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return false;
}
}
/**
* Protokolliert die Ausführungszeit einer Operation
*
* @param string $message Nachricht für die Protokollierung
* @return void
*/
private function logExecutionTime($message)
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info($message. ' | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
}
}

View file

@ -1,129 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\User;
use App\Services\ShoppingUserService;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class SyncShoppingUserData extends Command
{
//aufruf: php artisan shopping:sync-user-data --force --start=4
protected $signature = 'shopping:sync-user-data {user_id?} {--force} {--start=1}';
protected $description = 'Synchronisiere Shopping User Daten für einen oder alle User';
public function handle()
{
$userId = $this->argument('user_id');
$force = $this->option('force');
$startId = $this->option('start');
try {
if ($userId) {
$this->syncSingleUser($userId);
} else {
$this->syncAllUsers($force, $startId);
}
} catch (Exception $e) {
$this->error("Ein Fehler ist aufgetreten: " . $e->getMessage());
Log::error("Shopping User Sync Fehler: ", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return 1;
}
return 0;
}
private function syncAllUsers($force, $startId)
{
$this->info("Starte Synchronisierung für alle User ab ID: {$startId}...");
$count = 0;
$errors = [];
// Aktiviere SQL Query Logging für Debugging
DB::enableQueryLog();
User::where('id', '>=', $startId)
->orderBy('id')
->chunk(10, function($users) use (&$count, &$errors, $force) {
foreach($users as $user) {
try {
$this->info("\nVerarbeite User ID: {$user->id} ({$user->email})");
$this->syncUser($user);
$count++;
$this->info("✓ User ID {$user->id} erfolgreich synchronisiert");
} catch (Exception $e) {
$errorMessage = "Fehler bei User ID {$user->id} ({$user->email}): " . $e->getMessage();
$errors[] = $errorMessage;
$this->error($errorMessage);
// Log die letzten SQL Queries
Log::error("Letzte SQL Queries:", [
'queries' => DB::getQueryLog()
]);
if (!$force) {
throw $e;
}
}
}
});
$this->newLine();
$this->info("Synchronisierung abgeschlossen!");
$this->info("Gesamt synchronisierte User: {$count}");
if (count($errors) > 0) {
$this->warn("Es gab " . count($errors) . " Fehler während der Synchronisierung:");
foreach($errors as $error) {
$this->warn("- " . $error);
}
}
}
private function syncUser(User $user)
{
try {
$this->output->write(" Setze Faker Mail... ");
ShoppingUserService::setFakerMail($user);
$this->info("");
$this->output->write(" Synchronisiere Numbers... ");
ShoppingUserService::syncNumbersByEmail($user);
$this->info("");
$this->output->write(" Synchronisiere Orders... ");
ShoppingUserService::syncOrdersByEmail($user);
$this->info("");
} catch (Exception $e) {
throw new Exception($e->getMessage() . "\n" . $e->getTraceAsString());
}
}
private function syncSingleUser($userId)
{
$user = User::find($userId);
if (!$user) {
throw new Exception("User ID {$userId} nicht gefunden");
}
$this->info("Starte Synchronisierung für User ID {$userId}...");
try {
$this->syncUser($user);
$this->info("✓ Synchronisierung erfolgreich abgeschlossen");
} catch (Exception $e) {
throw new Exception("Fehler bei User ID {$userId}: " . $e->getMessage());
}
}
}

View file

@ -1,319 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\Services\BusinessPlan\BusinessUserItemOptimized;
use App\Services\BusinessPlan\GrowthBonusCalculator;
use App\User;
use App\Models\UserBusiness;
use Carbon\Carbon;
class TestGrowthBonusCalculation extends Command
{
protected $signature = 'test:growth-bonus
{user_id : Die User-ID für den Test}
{--month=12 : Der Monat}
{--year=2025 : Das Jahr}
{--debug : Zeigt detaillierte Debug-Informationen}
{--from-db : Verwendet gespeicherte Daten und simuliert Berechnung}';
protected $description = 'Testet die Growth Bonus Berechnung für einen User';
public function handle()
{
$userId = $this->argument('user_id');
$month = (int) $this->option('month');
$year = (int) $this->option('year');
$debug = $this->option('debug');
$fromDb = $this->option('from-db');
$this->info("=== Growth Bonus Test ===");
$this->info("User: {$userId}, Monat: {$month}/{$year}");
$this->newLine();
// User laden
$user = User::find($userId);
if (!$user) {
$this->error("User {$userId} nicht gefunden!");
return 1;
}
$date = Carbon::createFromDate($year, $month, 1);
// Gespeicherte UserBusiness-Daten laden
$userBusiness = UserBusiness::where('user_id', $userId)
->where('month', $month)
->where('year', $year)
->first();
if ($userBusiness) {
$this->info("=== GESPEICHERTE DATEN (UserBusiness) ===");
$qualLevel = $userBusiness->qual_user_level;
$this->table(
['Feld', 'Wert'],
[
['user_id', $userBusiness->user_id],
['qual_user_level (Name)', $qualLevel['name'] ?? 'NULL'],
['Growth Bonus (aus qual_user_level)', $qualLevel['growth_bonus'] ?? 'NULL'],
['active_growth_bonus (gespeichert)', $userBusiness->active_growth_bonus ?? 'NULL'],
['commission_growth_total', $userBusiness->commission_growth_total ?? 0],
]
);
} else {
$this->warn("Keine gespeicherten UserBusiness-Daten für {$month}/{$year}");
if (!$fromDb) {
$this->info("Versuche Live-Berechnung...");
} else {
$this->error("--from-db erfordert gespeicherte Daten!");
return 1;
}
}
if ($fromDb && $userBusiness) {
$this->testFromDatabase($userId, $month, $year, $debug);
} else {
$this->testLiveCalculation($user, $date, $month, $year, $debug);
}
$this->newLine();
$this->info("=== TEST ABGESCHLOSSEN ===");
return 0;
}
private function testFromDatabase(int $userId, int $month, int $year, bool $debug): void
{
$this->newLine();
$this->info("=== ANALYSE AUS DATENBANK ===");
// Lade User und seine Firstlines
$userBusiness = UserBusiness::where('user_id', $userId)
->where('month', $month)
->where('year', $year)
->first();
$qualLevel = $userBusiness->qual_user_level;
$myGrowthBonus = (float) ($qualLevel['growth_bonus'] ?? 0);
$this->info("Mein Growth Bonus Anspruch: {$myGrowthBonus}%");
if ($myGrowthBonus <= 0) {
$this->warn("Kein Growth Bonus Anspruch!");
return;
}
// Lade Firstlines
$firstlineIds = User::where('m_sponsor', $userId)->pluck('id')->toArray();
$this->info("Anzahl Firstlines: " . count($firstlineIds));
$this->newLine();
$this->info("=== FIRSTLINE ANALYSE ===");
$totalExpectedGrowth = 0;
$problemsFound = false;
foreach ($firstlineIds as $flId) {
$flBusiness = UserBusiness::where('user_id', $flId)
->where('month', $month)
->where('year', $year)
->first();
if (!$flBusiness) {
continue;
}
$flQualLevel = $flBusiness->qual_user_level;
$flGrowthBonus = (float) ($flQualLevel['growth_bonus'] ?? 0);
$flLevelName = $flQualLevel['name'] ?? 'Kein Level';
$flTPSum = (float) ($flBusiness->sales_volume_points_TP_sum ?? 0);
if ($flTPSum <= 0) {
continue;
}
// Berechne erwartete Differenz
$expectedDiff = max(0, $myGrowthBonus - $flGrowthBonus);
$expectedCommission = round($flTPSum / 100 * $expectedDiff, 2);
// Problem-Erkennung: Wenn FL einen Growth Bonus hat aber wir
// trotzdem den vollen Betrag bekommen
$isPotentialProblem = $flGrowthBonus > 0 && $expectedDiff < $myGrowthBonus;
$this->info("--- Firstline User {$flId} ---");
$this->table(
['Feld', 'Wert'],
[
['Level erreicht', $flLevelName],
['Growth Bonus (FL)', $flGrowthBonus . '%'],
['Team-Punkte (TP_sum)', number_format($flTPSum, 0, ',', '.')],
['Mein Anspruch', $myGrowthBonus . '%'],
['Differenz (erwartet)', $expectedDiff . '%'],
['Erwartete Provision', number_format($expectedCommission, 2, ',', '.') . ' €'],
['ACHTUNG: Blockade?', $isPotentialProblem ? 'JA - FL sollte blockieren!' : 'Nein'],
]
);
if ($isPotentialProblem) {
$problemsFound = true;
$this->error(" ⚠️ User {$flId} hat {$flLevelName} ({$flGrowthBonus}%) erreicht!");
$this->error(" Differenz sollte nur {$expectedDiff}% sein, nicht {$myGrowthBonus}%!");
}
$totalExpectedGrowth += $expectedCommission;
}
$this->newLine();
$this->info("=== ZUSAMMENFASSUNG ===");
$this->info("Erwartete Growth Bonus Summe (mit korrekter Differenz): " . number_format($totalExpectedGrowth, 2, ',', '.') . ' €');
$this->info("Gespeicherte Growth Bonus Summe: " . number_format($userBusiness->commission_growth_total ?? 0, 2, ',', '.') . ' €');
if ($problemsFound) {
$this->newLine();
$this->error("⚠️ PROBLEME GEFUNDEN! Die Blockade durch qualifizierte Firstlines funktioniert möglicherweise nicht korrekt.");
}
// Detailanalyse: Wie wurde die Berechnung durchgeführt?
if ($debug) {
$this->newLine();
$this->info("=== DEBUG: Rekursive Volumen-Analyse ===");
$this->analyzeVolumeDistribution($userId, $month, $year, $myGrowthBonus, 1);
}
}
private function analyzeVolumeDistribution(int $userId, int $month, int $year, float $myPercent, int $depth = 1): array
{
if ($depth > 10) {
return ['0.0' => 0];
}
$userBusiness = UserBusiness::where('user_id', $userId)
->where('month', $month)
->where('year', $year)
->first();
if (!$userBusiness) {
return ['0.0' => 0];
}
$qualLevel = $userBusiness->qual_user_level;
$myProtection = (float) ($qualLevel['growth_bonus'] ?? 0);
$indent = str_repeat(" ", $depth);
// Eigenes Volumen (TP_sum - aber nur direkte Punkte, nicht Team)
// Bei gespeicherten Daten ist das schwer zu unterscheiden
// Vereinfachung: Für den Test nehmen wir TP_sum als Gesamt-Volumen
$this->line("{$indent}User {$userId}: Protection={$myProtection}%");
// Lade Kinder
$childIds = User::where('m_sponsor', $userId)->pluck('id')->toArray();
$volumes = [];
foreach ($childIds as $childId) {
$childBusiness = UserBusiness::where('user_id', $childId)
->where('month', $month)
->where('year', $year)
->first();
if (!$childBusiness) {
continue;
}
$childTP = (float) ($childBusiness->sales_volume_points_TP_sum ?? 0);
if ($childTP <= 0) {
continue;
}
$childQual = $childBusiness->qual_user_level;
$childProtection = (float) ($childQual['growth_bonus'] ?? 0);
$childLevelName = $childQual['name'] ?? 'Kein Level';
// Berechne Differenz
$diff = max(0, $myPercent - $childProtection);
$commission = round($childTP / 100 * $diff, 2);
$this->line("{$indent} └─ Child {$childId}: {$childLevelName}, Protection={$childProtection}%, TP={$childTP}");
$this->line("{$indent} Differenz: {$myPercent}% - {$childProtection}% = {$diff}%");
$this->line("{$indent} Provision: {$childTP} * {$diff}% = {$commission}");
if ($childProtection > 0) {
$this->warn("{$indent} ⚠️ BLOCKADE durch {$childLevelName}!");
}
}
return $volumes;
}
private function testLiveCalculation(User $user, Carbon $date, int $month, int $year, bool $debug): void
{
$this->newLine();
$this->info("=== LIVE-BERECHNUNG ===");
// TreeCalcBot erstellen
$treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', true);
// BusinessUserItem erstellen
$businessUserItem = new BusinessUserItemOptimized($date, $treeCalcBot);
$businessUserItem->makeUserFromModel($user, true);
$businessUserItem->addUserID();
// Kinder laden
$businessUserItem->readParentsBusinessUsers(true, 0);
// Qualifikation berechnen
$businessUserItem->calcQualPP(true);
$qualUserLevel = $businessUserItem->getQualUserLevel();
$this->table(
['Feld', 'Wert'],
[
['user_id', $businessUserItem->user_id],
['isQualLevel()', $businessUserItem->isQualLevel() ? 'JA' : 'NEIN'],
['isQualificationCalculated()', $businessUserItem->isQualificationCalculated() ? 'JA' : 'NEIN'],
['getQualUserLevel() (Name)', $qualUserLevel['name'] ?? 'NULL'],
['Growth Bonus (qual_user_level)', $qualUserLevel['growth_bonus'] ?? 'NULL'],
['getActiveGrowthBonus()', $businessUserItem->getActiveGrowthBonus()],
['getQualifiedGrowthBonus()', $businessUserItem->getQualifiedGrowthBonus()],
]
);
$this->newLine();
$this->info("=== FIRSTLINES (Kinder) ===");
if (empty($businessUserItem->businessUserItems)) {
$this->warn("Keine Firstlines geladen!");
} else {
foreach ($businessUserItem->businessUserItems as $index => $childItem) {
$childQual = $childItem->getQualUserLevel();
$this->info("--- Firstline " . ($index + 1) . " ---");
$this->table(
['Feld', 'Wert'],
[
['user_id', $childItem->user_id],
['isQualLevel()', $childItem->isQualLevel() ? 'JA' : 'NEIN'],
['getQualUserLevel() (Name)', $childQual['name'] ?? 'NULL'],
['Growth Bonus (qual_user_level)', $childQual['growth_bonus'] ?? 'NULL'],
['getActiveGrowthBonus()', $childItem->getActiveGrowthBonus()],
['getQualifiedGrowthBonus()', $childItem->getQualifiedGrowthBonus()],
['sales_volume_points_TP_sum', $childItem->sales_volume_points_TP_sum ?? 0],
]
);
}
}
if ($qualUserLevel && ($qualUserLevel['growth_bonus'] ?? 0) > 0) {
$calculator = new GrowthBonusCalculator();
$qualData = (object) $qualUserLevel;
$totalGrowthBonus = $calculator->calculate($businessUserItem, $qualData);
$this->newLine();
$this->info("Berechneter Growth Bonus: {$totalGrowthBonus}");
}
}
}

View file

@ -1,321 +0,0 @@
<?php
namespace App\Console\Commands;
use App\User;
use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\Cron\UserLevelUpdate;
use App\Console\Commands\BusinessStoreOptimized;
use Illuminate\Console\Command;
use ReflectionClass;
class TestUserLevelUpdate extends Command
{
/**
* php artisan test:user-level-update {month} {year} {--user_id=} {--send-mail} {--dry-run}
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:user-level-update {month} {year} {--user_id= : Test für spezifischen User} {--send-mail : E-Mail senden} {--dry-run : Nur anzeigen, nicht speichern}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Testet die UserLevelUpdate-Funktion aus BusinessStoreOptimized für einen Monat/Jahr oder spezifischen User';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$month = (int) $this->argument('month');
$year = (int) $this->argument('year');
$userId = $this->option('user_id') ? (int) $this->option('user_id') : null;
$sendMail = $this->option('send-mail');
$dryRun = $this->option('dry-run');
$this->info("===========================================");
$this->info("UserLevelUpdate Test");
$this->info("===========================================");
$this->line("Monat: {$month}");
$this->line("Jahr: {$year}");
if ($userId) {
$this->line("User ID: {$userId}");
}
$this->line("E-Mail senden: " . ($sendMail ? 'Ja' : 'Nein'));
$this->line("Dry-Run (nur zeigen): " . ($dryRun ? 'Ja' : 'Nein'));
$this->line("");
if ($dryRun) {
// Im Dry-Run Modus zeigen wir nur die Analyse
$this->performDryRunAnalysis($month, $year, $userId);
} else {
// Nutze die originale Funktion aus BusinessStoreOptimized
$this->runOriginalFunction($month, $year, $sendMail);
}
$this->info("");
$this->info("===========================================");
$this->info("Test abgeschlossen!");
$this->info("===========================================");
return 0;
} catch (\Exception $e) {
$this->error('Test fehlgeschlagen: ' . $e->getMessage());
$this->error('Stack trace: ' . $e->getTraceAsString());
return 1;
}
}
/**
* Führt die originale userLevelUpdate Funktion aus BusinessStoreOptimized aus
*/
private function runOriginalFunction(int $month, int $year, bool $sendMail)
{
$this->info("Erstelle BusinessStoreOptimized Instanz...");
// Erstelle BusinessStoreOptimized Command-Instanz
$businessStoreCommand = new BusinessStoreOptimized();
// Setze Output auf aktuellen Command (damit Ausgaben weitergeleitet werden)
$businessStoreCommand->setOutput($this->output);
// Setze Monat und Jahr über Reflection (da private)
$reflection = new ReflectionClass($businessStoreCommand);
$monthProperty = $reflection->getProperty('month');
$monthProperty->setAccessible(true);
$monthProperty->setValue($businessStoreCommand, $month);
$yearProperty = $reflection->getProperty('year');
$yearProperty->setAccessible(true);
$yearProperty->setValue($businessStoreCommand, $year);
$timeStartProperty = $reflection->getProperty('timeStart');
$timeStartProperty->setAccessible(true);
$timeStartProperty->setValue($businessStoreCommand, microtime(true));
// Setze sendUpdateMail
$businessStoreCommand->setSendUpdateMail($sendMail);
$this->info("Führe originale userLevelUpdate() Funktion aus...");
$this->line("");
// Rufe die originale Funktion auf
$businessStoreCommand->userLevelUpdate();
}
/**
* Führt Dry-Run Analyse durch
*/
private function performDryRunAnalysis(int $month, int $year, ?int $userId)
{
$userLevelUpdate = new UserLevelUpdate($month, $year);
if ($userId) {
// Test für spezifischen User
$this->testSingleUserDryRun($userLevelUpdate, $userId);
} else {
// Test für alle User
$this->testAllUsersDryRun($userLevelUpdate);
}
}
/**
* Dry-Run Analyse für einen spezifischen User
*/
private function testSingleUserDryRun(UserLevelUpdate $userLevelUpdate, int $userId)
{
$userBusiness = UserBusiness::with('user')
->where('month', $this->argument('month'))
->where('year', $this->argument('year'))
->where('user_id', $userId)
->whereNotNull('next_qual_user_level')
->whereRaw("JSON_LENGTH(next_qual_user_level) > 0")
->first();
if (!$userBusiness) {
$this->warn("Keine UserBusiness mit next_qual_user_level gefunden für User ID: {$userId}");
// Zeige vorhandene UserBusiness-Daten
$anyUserBusiness = UserBusiness::where('user_id', $userId)
->where('month', $this->argument('month'))
->where('year', $this->argument('year'))
->first();
if ($anyUserBusiness) {
$this->info("UserBusiness existiert, aber hat kein next_qual_user_level");
$this->line("Current Level ID: " . ($anyUserBusiness->m_level_id ?? 'NULL'));
$this->line("next_qual_user_level: " . (is_null($anyUserBusiness->next_qual_user_level) ? 'NULL' : json_encode($anyUserBusiness->next_qual_user_level)));
} else {
$this->warn("Keine UserBusiness gefunden für diesen Monat/Jahr");
}
return;
}
$this->displayUserBusinessInfo($userBusiness);
$this->info("");
$this->info("DRY-RUN MODUS: Änderungen werden nicht gespeichert");
$this->analyzeLevelUpdate($userBusiness);
}
/**
* Dry-Run Analyse für alle User
*/
private function testAllUsersDryRun(UserLevelUpdate $userLevelUpdate)
{
$levelUpdateUsers = $userLevelUpdate->getUserBusinessByMonthYear();
$this->info("Gefunden: " . $levelUpdateUsers->count() . " UserBusiness-Einträge mit next_qual_user_level");
$this->line("");
if ($levelUpdateUsers->count() === 0) {
$this->warn("Keine UserBusiness-Einträge mit Level-Updates gefunden.");
return;
}
$this->info("DRY-RUN MODUS: Änderungen werden nicht gespeichert");
$this->line("");
foreach ($levelUpdateUsers as $userBusiness) {
$this->line("---------------------------------------------------");
$this->displayUserBusinessInfo($userBusiness);
$this->analyzeLevelUpdate($userBusiness);
$this->line("");
}
}
/**
* Zeigt Informationen über UserBusiness
*/
private function displayUserBusinessInfo(UserBusiness $userBusiness)
{
$user = $userBusiness->user;
$this->line("User ID: " . ($user ? $user->id : 'NULL'));
$this->line("E-Mail: " . ($user ? $user->email : 'N/A'));
$this->line("Aktuelles Level: " . ($user && $user->m_level ? $user->m_level . ' (' . $this->getLevelName($user->m_level) . ')' : 'Kein Level'));
$this->line("UserBusiness Level ID: " . ($userBusiness->m_level_id ?? 'NULL'));
$this->line("UserBusiness Level Name: " . ($userBusiness->user_level_name ?? 'NULL'));
$nextQual = $userBusiness->next_qual_user_level;
if (is_array($nextQual)) {
if (isset($nextQual['id'])) {
// Einzelnes Level
$this->line("Nächstes qualifiziertes Level: ID " . $nextQual['id'] . ' - ' . ($nextQual['name'] ?? 'N/A') . ' (POS: ' . ($nextQual['pos'] ?? 'N/A') . ')');
$this->line(" Bereits aktualisiert: " . (isset($nextQual['hasUpdated']) && $nextQual['hasUpdated'] == 1 ? 'Ja' : 'Nein'));
} else {
// Array von Leveln
$this->line("Nächste qualifizierte Level: " . count($nextQual) . " Level gefunden");
foreach ($nextQual as $idx => $level) {
if (is_array($level) && isset($level['id'])) {
$updated = isset($level['hasUpdated']) && $level['hasUpdated'] == 1 ? ' (bereits aktualisiert)' : '';
$this->line(" [{$idx}] ID " . $level['id'] . ' - ' . ($level['name'] ?? 'N/A') . ' (POS: ' . ($level['pos'] ?? 'N/A') . ')' . $updated);
}
}
}
} else {
$this->line("next_qual_user_level: " . (is_null($nextQual) ? 'NULL' : gettype($nextQual)));
}
$this->line("Total Qual PP: " . ($userBusiness->total_qual_pp ?? 0));
}
/**
* Analysiert ob und wie ein Level-Update durchgeführt würde (Dry-Run)
*/
private function analyzeLevelUpdate(UserBusiness $userBusiness)
{
$user = $userBusiness->user;
if (!$user) {
$this->warn("⚠ Kein User-Objekt vorhanden");
return;
}
$nextQual = $userBusiness->next_qual_user_level;
if (!is_array($nextQual) || empty($nextQual)) {
$this->warn("⚠ next_qual_user_level ist kein gültiges Array");
return;
}
// Lade UserLevels für Vergleich
$userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
// Prüfe ob einzelnes Level oder Array
$levelArray = isset($nextQual['id']) ? [$nextQual] : $nextQual;
$currentUserLevel = null;
if ($user->m_level) {
$currentUserLevel = $userLevels->get($user->m_level);
}
$this->info("");
$this->info("📊 Analyse:");
if ($currentUserLevel) {
$this->line(" Aktuelles Level POS: {$currentUserLevel->pos}");
} else {
$this->line(" Aktuelles Level: Kein Level gesetzt");
}
$wouldUpdate = false;
$highestLevel = null;
$highestPos = 0;
foreach ($levelArray as $levelData) {
if (!is_array($levelData) || !isset($levelData['id'])) {
continue;
}
if (isset($levelData['hasUpdated']) && $levelData['hasUpdated'] == 1) {
$this->line(" ⏭ Level ID {$levelData['id']} wurde bereits aktualisiert");
continue;
}
$newLevel = $userLevels->get($levelData['id']);
$newLevelPos = $newLevel ? $newLevel->pos : ($levelData['pos'] ?? 0);
$levelName = $levelData['name'] ?? 'N/A';
$this->line(" 📈 Level ID {$levelData['id']} ({$levelName}): POS {$newLevelPos}");
if (!$currentUserLevel || $newLevelPos > $currentUserLevel->pos) {
if ($newLevelPos > $highestPos) {
$highestPos = $newLevelPos;
$highestLevel = $levelData;
$wouldUpdate = true;
}
} else {
$this->line(" ⚠ Level ist nicht höher als aktuelles Level (POS {$currentUserLevel->pos})");
}
}
if ($wouldUpdate && $highestLevel) {
$this->info("");
$highestLevelName = $highestLevel['name'] ?? 'N/A';
$this->info("✅ Würde Level aktualisieren zu: {$highestLevel['id']} ({$highestLevelName})");
$this->line(" Von: " . ($currentUserLevel ? "POS {$currentUserLevel->pos}" : "Kein Level") . " → Zu: POS {$highestPos}");
} else {
$this->info("");
$this->warn("⚠ Kein Level-Update würde durchgeführt:");
if (!$wouldUpdate) {
$this->line(" Kein höheres Level gefunden");
}
}
}
/**
* Holt Level-Name nach ID
*/
private function getLevelName($levelId)
{
$level = UserLevel::find($levelId);
return $level ? $level->name : 'Unbekannt';
}
}

View file

@ -1,387 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\AboHelper;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class TestUserMakeAboOrder extends Command
{
/**
* php artisan test:user-make-abo-order {--abo_id=} {--date=} {--dry-run} {--force}
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:user-make-abo-order
{--abo_id= : Test für spezifisches Abo (ID)}
{--date= : Test-Datum im Format Y-m-d (Standard: heute)}
{--dry-run : Nur anzeigen, keine Bestellung erstellen}
{--force : Überschreibt Duplikatsprüfung}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Testet die Abo-Bestellungserstellung für ein oder mehrere Abos';
private $timeStart;
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->timeStart = microtime(true);
$this->info('=== Test: UserMakeAboOrder ===');
$this->newLine();
try {
$aboId = $this->option('abo_id');
$testDate = $this->option('date') ? Carbon::parse($this->option('date'))->format('Y-m-d') : Carbon::now()->format('Y-m-d');
$dryRun = $this->option('dry-run');
$force = $this->option('force');
$this->info("Test-Datum: {$testDate}");
if ($dryRun) {
$this->warn('DRY-RUN Modus: Es werden keine Bestellungen erstellt!');
}
if ($force) {
$this->warn('FORCE Modus: Duplikatsprüfung wird überschrieben!');
}
$this->newLine();
if ($aboId) {
// Test für spezifisches Abo
$userAbo = UserAbo::find($aboId);
if (! $userAbo) {
$this->error("Abo mit ID {$aboId} nicht gefunden!");
return 1;
}
$this->testSingleAbo($userAbo, $testDate, $dryRun, $force);
} else {
// Test für alle fälligen Abos
$this->testAllAbos($testDate, $dryRun, $force);
}
$executionTime = $this->getExecutionTime();
$this->newLine();
$this->info("Test erfolgreich abgeschlossen in {$executionTime}");
return 0;
} catch (\Exception $e) {
$this->error('Fehler beim Testen: '.$e->getMessage());
$this->error($e->getTraceAsString());
return 1;
}
}
/**
* Testet ein einzelnes Abo
*
* @param UserAbo $userAbo
* @param string $testDate
* @param bool $dryRun
* @param bool $force
* @return void
*/
private function testSingleAbo($userAbo, $testDate, $dryRun, $force)
{
$this->info("Teste Abo ID: {$userAbo->id}");
$this->displayAboInfo($userAbo);
// Prüfe ob Abo für Test-Datum fällig ist
if ($userAbo->next_date != $testDate && ! $force) {
$this->warn("Abo ist nicht für {$testDate} fällig (next_date: {$userAbo->next_date})");
if (! $this->confirm('Trotzdem fortfahren?', false)) {
return;
}
}
// Prüfe auf Duplikate
if (! $force) {
$existingOrder = UserAboOrder::where('user_abo_id', $userAbo->id)
->whereDate('created_at', $testDate)
->first();
if ($existingOrder) {
$this->warn("Es existiert bereits eine Bestellung für dieses Abo am {$testDate}");
$this->info("Bestell-ID: {$existingOrder->shopping_order_id}");
if (! $this->confirm('Trotzdem fortfahren?', false)) {
return;
}
}
}
$this->newLine();
$this->info('Starte Bestellungserstellung...');
if ($dryRun) {
$this->info('[DRY-RUN] Bestellung würde erstellt werden');
$this->displayOrderPreview($userAbo);
} else {
// Temporär next_date setzen für Test
$originalNextDate = $userAbo->next_date;
if ($userAbo->next_date != $testDate) {
$userAbo->next_date = $testDate;
$userAbo->save();
$this->info("Temporär next_date auf {$testDate} gesetzt");
}
try {
$shoppingOrder = $this->makeOrder($userAbo, $dryRun);
if ($shoppingOrder) {
$this->info("✓ Bestellung erfolgreich erstellt: ID {$shoppingOrder->id}");
} else {
$this->error('✗ Bestellung konnte nicht erstellt werden');
}
} finally {
// next_date zurücksetzen falls geändert
if ($originalNextDate != $testDate) {
$userAbo->next_date = $originalNextDate;
$userAbo->save();
$this->info("next_date zurückgesetzt auf {$originalNextDate}");
}
}
}
}
/**
* Testet alle fälligen Abos
*
* @param string $testDate
* @param bool $dryRun
* @param bool $force
* @return void
*/
private function testAllAbos($testDate, $dryRun, $force)
{
$query = UserAbo::where('next_date', '=', $testDate)
->where('active', true);
if (! $force) {
$query->whereDoesntHave('user_abo_orders', function ($q) use ($testDate) {
$q->whereDate('created_at', $testDate);
});
}
$userAbos = $query->get();
$count = $userAbos->count();
$this->info("Gefundene fällige Abos: {$count}");
$this->newLine();
if ($count === 0) {
$this->warn('Keine fälligen Abos gefunden!');
return;
}
if (! $this->confirm("Möchten Sie {$count} Abo(s) testen?", true)) {
return;
}
$this->newLine();
foreach ($userAbos as $userAbo) {
$this->info("--- Abo ID: {$userAbo->id} ---");
$this->testSingleAbo($userAbo, $testDate, $dryRun, $force);
$this->newLine();
}
}
/**
* Zeigt Informationen über ein Abo an
*
* @param UserAbo $userAbo
* @return void
*/
private function displayAboInfo($userAbo)
{
$this->table(
['Feld', 'Wert'],
[
['ID', $userAbo->id],
['User ID', $userAbo->user_id],
['Payone UserID', $userAbo->payone_userid],
['Aktiv', $userAbo->active ? 'Ja' : 'Nein'],
['Status', $userAbo->status.' ('.($userAbo->getStatusType() ?? 'unbekannt').')'],
['Intervall', $userAbo->abo_interval],
['Next Date', $userAbo->next_date],
['Last Date', $userAbo->last_date ?? 'Nie'],
['Amount', number_format($userAbo->amount / 100, 2, ',', '.').' €'],
['is_for', $userAbo->is_for],
['Clearing Type', $userAbo->clearingtype],
['Items', $userAbo->user_abo_items->count()],
]
);
// Zeige Abo-Items
if ($userAbo->user_abo_items->count() > 0) {
$this->info('Abo-Items:');
$items = [];
foreach ($userAbo->user_abo_items as $item) {
$items[] = [
'Product ID' => $item->product_id,
'Qty' => $item->qty,
'Comp' => $item->comp ?? '-',
'Price' => number_format($item->price / 100, 2, ',', '.').' €',
];
}
$this->table(['Product ID', 'Qty', 'Comp', 'Price'], $items);
}
}
/**
* Zeigt eine Vorschau der Bestellung an
*
* @param UserAbo $userAbo
* @return void
*/
private function displayOrderPreview($userAbo)
{
$this->info('Bestell-Vorschau:');
$this->info('- Shopping-User würde erstellt/aktualisiert');
$this->info('- Bestellung würde mit folgenden Items erstellt:');
foreach ($userAbo->user_abo_items as $item) {
$product = $item->product;
$this->info(" • Product ID {$item->product_id}: {$item->qty}x");
}
$this->info('- Zahlung würde durchgeführt');
}
/**
* Erstellt eine Bestellung für ein Abo (vereinfachte Version für Test)
*
* @param UserAbo $userAbo
* @param bool $dryRun
* @return mixed
*/
private function makeOrder($userAbo, $dryRun = false)
{
$this->info('Erstelle Shopping-User...');
$userOrder = new UserMakeOrder($userAbo);
if (! $userOrder->createShoppingUser()) {
$this->error('Konnte Shopping-User nicht erstellen');
return null;
}
$this->info('✓ Shopping-User erstellt');
$this->info('Erstelle Bestellung...');
$shoppingOrder = $userOrder->makeShoppingOrder();
$shoppingOrder->mode = 'test'; // immer im test mode testen
$shoppingOrder->save();
if (! $shoppingOrder) {
$this->error('Konnte Bestellung nicht erstellen');
return null;
}
$this->info("✓ Bestellung erstellt: ID {$shoppingOrder->id}");
if ($dryRun) {
$this->info('[DRY-RUN] Zahlung würde durchgeführt');
$this->info('[DRY-RUN] Abo würde aktualisiert');
return $shoppingOrder;
}
$this->info('Starte Zahlungsvorgang...');
try {
$response = $userOrder->makePayment();
if (is_object($response)) {
$response = (array) $response;
}
$this->info('Zahlungsantwort: '.json_encode($response, JSON_PRETTY_PRINT));
if (! isset($response['status'])) {
$this->warn('⚠ Kein Status in Zahlungsantwort');
return $shoppingOrder;
}
if ($response['status'] === 'APPROVED') {
$this->info('✓ Zahlung erfolgreich');
$this->info('Aktualisiere Abo...');
$this->updateAbo($userAbo, $shoppingOrder, 1);
$this->info('✓ Abo aktualisiert');
} elseif ($response['status'] === 'ERROR') {
$this->error('✗ Zahlungsfehler');
$this->warn('Abo wird beim nächsten Cron-Lauf erneut versucht');
} else {
$this->warn("⚠ Zahlungsstatus: {$response['status']}");
}
} catch (\Exception $e) {
$this->error('Fehler bei Zahlung: '.$e->getMessage());
}
return $shoppingOrder;
}
/**
* Aktualisiert das Abo nach erfolgreicher Bestellung (vereinfachte Version)
*
* @param UserAbo $userAbo
* @param mixed $shoppingOrder
* @param int $status
* @return void
*/
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
{
try {
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
$updateData = [
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
'last_date' => now(),
];
if ($status !== 1) {
$updateData['status'] = $status;
}
$userAbo->update($updateData);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => $status,
'paid' => true,
]);
});
} catch (\Exception $e) {
$this->error('Fehler beim Aktualisieren des Abos: '.$e->getMessage());
throw $e;
}
}
/**
* Berechnet die Ausführungszeit
*
* @return string
*/
private function getExecutionTime()
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
return $sec.' Sekunden und '.round($micro * 1000, 2).' ms';
}
}

View file

@ -1,203 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\UserUtil;
use App\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
class UserCleanUp extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:cleanup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'User Clean Up inactive for Business Structure and UserDetails';
private $timeStart;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('RUN Command user:cleanup');
\Log::channel('cleanup')->info('COMMAND [user:cleanup] started.');
$this->timeStart = microtime(true);
// Schritt 1: User löschen, die länger als 2 Monate inaktiv sind
$this->deleteInactiveUsers();
// Schritt 2: Alle inaktiven User deaktivieren (länger als 2 Wochen inaktiv)
// Ihre Downline wird dem nächsten aktiven Berater (Sponsor) zugewiesen
$this->cleanUpInActiveUser();
\Log::channel('cleanup')->info('COMMAND [user:cleanup] finished.');
return 0;
}
/**
* Löscht User, die länger als 2 Monate inaktiv sind (payment_account < -2 month)
* - Weist deren Vertriebspartner-Kinder dem nächsten aktiven Sponsor zu
* - Überträgt deren Shopping-Kunden zum neuen Sponsor
* - Konvertiert den User zu einem Shopping-Kunden
* - Löscht den User (soft delete)
*/
private function deleteInactiveUsers()
{
$methodStartTime = microtime(true);
$this->info('START Command deleteInactiveUsers');
$count = 0;
$date = Carbon::now()->modify('-2 month');
$delete_users = User::where('admin', 0)->where('payment_account', '<', $date)->get();
foreach ($delete_users as $delete_user) {
\DB::beginTransaction();
try {
// Finde nächsten aktiven Sponsor
$active_sponsor = UserUtil::findNextActiveSponsor($delete_user->id);
if (! $active_sponsor) {
\Log::channel('cleanup')->error('deleteInactiveUsers find no active_sponsor by delete_user_id: '.$delete_user->id);
\DB::rollBack();
continue;
}
// Prüfe ob User Account-Daten hat
if (! $delete_user->account) {
\Log::channel('cleanup')->error('deleteInactiveUsers: User has no account data, skipping user_id: '.$delete_user->id);
\DB::rollBack();
continue;
}
// Setze alle Vertriebspartner-Kinder zum neuen Sponsor
UserUtil::setNewSponsorToChilds($delete_user->id, $active_sponsor->id);
// Übertrage Shopping-User zum neuen Sponsor
UserUtil::setShoppingUserToNewMember($delete_user->id, $active_sponsor->id);
// Konvertiere User zu Client beim neuen Sponsor
UserUtil::setUserToClient($delete_user->id, $active_sponsor->id);
$data = [
'user_id' => $delete_user->id,
'email' => $delete_user->email,
'm_account' => $delete_user->account->m_account,
'm_first_name' => $delete_user->account->m_first_name,
'm_last_name' => $delete_user->account->m_last_name,
];
// Lösche User (soft delete)
UserUtil::deleteUser($delete_user);
\DB::commit();
$count++;
\Log::channel('cleanup')->info('deleteUser: '.json_encode($data));
} catch (\Exception $e) {
\DB::rollBack();
\Log::channel('cleanup')->error('deleteInactiveUsers failed for user_id: '.$delete_user->id.' | Error: '.$e->getMessage());
continue;
}
}
$diff = microtime(true) - $methodStartTime;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command deleteInactiveUsers: '.$count.' | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms');
}
/**
* Deaktiviert User, die länger als 2 Wochen inaktiv sind
* - Weist deren Vertriebspartner-Kinder dem nächsten aktiven Sponsor zu
* - Deaktiviert den User (behält Account, speichert Sponsor in pre_sponsor)
* - Shopping-Kunden werden NICHT übertragen (bleiben beim deaktivierten User)
*/
private function cleanUpInActiveUser()
{
$methodStartTime = microtime(true);
$this->info('START Command cleanUpInActiveUser');
$count = 0;
// Finde User die länger als 2 Wochen inaktiv sind
$date = Carbon::now()->modify('-2 weeks');
$inactive_users = User::where('active', true)
->where('m_sponsor', '!=', null)
->where('payment_account', '<', $date)
->get();
foreach ($inactive_users as $inactive_user) {
\DB::beginTransaction();
try {
// Finde nächsten aktiven Sponsor
$active_sponsor = UserUtil::findNextActiveSponsor($inactive_user->m_sponsor);
if (! $active_sponsor) {
\Log::channel('cleanup')->error('cleanUpInActiveUser find no active_sponsor by inactive_user: '.$inactive_user->id);
\DB::rollBack();
continue;
}
// Setze alle Vertriebspartner-Kinder zum neuen Sponsor
UserUtil::setNewSponsorToChilds($inactive_user->id, $active_sponsor->id);
$data = [
'user_id' => $inactive_user->id,
'email' => $inactive_user->email,
'm_account' => $inactive_user->account ? $inactive_user->account->m_account : '',
'm_first_name' => $inactive_user->account ? $inactive_user->account->m_first_name : '',
'm_last_name' => $inactive_user->account ? $inactive_user->account->m_last_name : '',
];
// Deaktiviere User (setzt pre_sponsor, entfernt m_sponsor, setzt active=false)
UserUtil::deactiveUser($inactive_user);
\DB::commit();
$count++;
\Log::channel('cleanup')->info('inactive_user: '.json_encode($data));
} catch (\Exception $e) {
\DB::rollBack();
\Log::channel('cleanup')->error('cleanUpInActiveUser failed for user_id: '.$inactive_user->id.' | Error: '.$e->getMessage());
continue;
}
}
$diff = microtime(true) - $methodStartTime;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('END Command cleanUpInActiveUser: '.$count.' | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms');
}
}

View file

@ -1,442 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\AboHelper;
use App\Services\Incentive\IncentiveTracker;
use App\Services\MyLog;
use App\Services\Payment;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class UserMakeAboOrder extends Command
{
/**
* ln -sfv /usr/bin/php73 /usr/bin/php
* php artisan business:store month year
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:make_abo_order';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Make Orders from Abos';
private $timeStart;
private $month;
private $year;
private $sendCreditMail = false;
private $sendUpdateMail = false;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->timeStart = microtime(true);
\Log::channel('cron')->info('UserMakeAboOrder: Befehl gestartet');
$this->info('RUN Command user:make_abo_order');
try {
$this->checkAbosToOrder();
$executionTime = $this->getExecutionTime();
\Log::channel('cron')->info("UserMakeAboOrder: Befehl erfolgreich abgeschlossen in {$executionTime}");
$this->info("Befehl erfolgreich abgeschlossen in {$executionTime}");
return 0;
} catch (\Exception $e) {
\Log::channel('cron')->error('UserMakeAboOrder: Fehler beim Ausführen des Befehls', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error('Fehler beim Ausführen des Befehls: ' . $e->getMessage());
return 1;
}
}
/**
* Prüft alle Abos, die heute fällig sind und erstellt Bestellungen
*
* @return void
*/
private function checkAbosToOrder()
{
$dateNow = Carbon::now()->format('Y-m-d');
\Log::channel('abo_order')->info('UserMakeAboOrder: Suche nach fälligen Abos für Datum', ['date' => $dateNow]);
// Prüfe auf bereits verarbeitete Abos am heutigen Tag (Duplikatsprüfung)
$userAbos = UserAbo::where('next_date', '=', $dateNow)
->where('active', true)
->where('status', '=', 2) // abo_okay
->whereDoesntHave('user_abo_orders', function ($query) use ($dateNow) {
$query->whereDate('created_at', $dateNow);
})
->get();
$count = $userAbos->count();
\Log::channel('abo_order')->info("UserMakeAboOrder: {$count} fällige Abos gefunden (ohne bereits verarbeitete)");
$this->info("Gefundene fällige Abos: {$count}");
foreach ($userAbos as $userAbo) {
\Log::channel('abo_order')->info('UserMakeAboOrder: Verarbeite Abo', [
'abo_id' => $userAbo->id,
'payone_userid' => $userAbo->payone_userid,
]);
$this->info("Verarbeite Abo: {$userAbo->id} (PayoneUserid: {$userAbo->payone_userid})");
try {
// Locking-Mechanismus: Verhindert Race Conditions bei paralleler Ausführung
$shoppingOrder = DB::transaction(function () use ($userAbo, $dateNow) {
// Lock das Abo für Update, um Race Conditions zu vermeiden
$lockedAbo = UserAbo::where('id', $userAbo->id)
->where('next_date', '=', $dateNow)
->where('active', true)
->where('status', '=', 2) // abo_okay
->lockForUpdate()
->first();
if (! $lockedAbo) {
\Log::channel('abo_order')->warning('UserMakeAboOrder: Abo wurde bereits verarbeitet oder ist nicht mehr aktiv', [
'abo_id' => $userAbo->id,
]);
return null;
}
// Nochmalige Prüfung auf Duplikat innerhalb der Transaktion
$existingOrder = UserAboOrder::where('user_abo_id', $lockedAbo->id)
->whereDate('created_at', $dateNow)
->first();
if ($existingOrder) {
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo wurde bereits heute verarbeitet', [
'abo_id' => $lockedAbo->id,
'existing_order_id' => $existingOrder->shopping_order_id,
]);
return null;
}
return $this->makeOrder($lockedAbo);
}, 3); // 3 Versuche bei Deadlocks
if ($shoppingOrder) {
\Log::channel('abo_order')->info('UserMakeAboOrder: Bestellung erstellt', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
]);
$this->info("Bestellung erstellt: {$shoppingOrder->id}");
} else {
\Log::channel('abo_order')->warning('UserMakeAboOrder: Keine Bestellung erstellt für Abo', ['abo_id' => $userAbo->id]);
$this->warn("Keine Bestellung erstellt für Abo: {$userAbo->id}");
}
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler bei der Verarbeitung des Abos', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error("Fehler bei Abo {$userAbo->id}: " . $e->getMessage());
}
}
}
/**
* Erstellt eine Bestellung für ein Abo
*
* @param UserAbo $userAbo
* @return mixed
*/
private function makeOrder($userAbo)
{
\Log::channel('abo_order')->info('UserMakeAboOrder: Starte Bestellungserstellung', ['abo_id' => $userAbo->id]);
$this->info('Starte Bestellungserstellung für Abo: ' . $userAbo->id);
$shoppingOrder = null;
$userOrder = new UserMakeOrder($userAbo);
try {
if (! $userOrder->createShoppingUser()) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Konnte Shopping-User nicht erstellen', ['abo_id' => $userAbo->id]);
$this->error("Konnte Shopping-User für Abo {$userAbo->id} nicht erstellen");
return null;
}
$shoppingOrder = $userOrder->makeShoppingOrder();
if (! $shoppingOrder) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Konnte Bestellung nicht erstellen', ['abo_id' => $userAbo->id]);
$this->error("Konnte Bestellung für Abo {$userAbo->id} nicht erstellen");
return null;
}
\Log::channel('abo_order')->info('UserMakeAboOrder: Bestellung erstellt, starte Zahlungsvorgang', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
]);
$response = $userOrder->makePayment();
$this->info('makePayment response: ' . json_encode($response));
// Prüfe ob Response ein Array ist (kann auch Objekt sein)
if (is_object($response)) {
$response = (array) $response;
}
if (! isset($response['status'])) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Ungültige Zahlungsantwort', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'response' => $response,
]);
$this->error("Ungültige Zahlungsantwort für Abo {$userAbo->id}");
// Bei fehlender Status-Information: Abo nicht aktualisieren, damit es beim nächsten Lauf erneut versucht wird
// Aber Bestellung speichern für Nachverfolgung
$this->updateAboOnError($userAbo, $shoppingOrder, 'Ungültige Zahlungsantwort - kein Status');
return $shoppingOrder;
}
if ($response['status'] === 'APPROVED') {
\Log::channel('abo_order')->info('UserMakeAboOrder: Zahlung erfolgreich', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'response' => $response,
]);
$this->info("Zahlung erfolgreich für Abo {$userAbo->id}");
// Nur bei erfolgreicher Zahlung: next_date aktualisieren
$this->updateAbo($userAbo, $shoppingOrder, 1);
} elseif ($response['status'] === 'ERROR') {
\Log::channel('abo_order')->error('UserMakeAboOrder: Zahlungsfehler', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'error' => $response,
]);
$this->error("Zahlungsfehler für Abo {$userAbo->id}");
MyLog::writeLog(
'userabo',
'error',
'Error:3002 App\Console\Commands\UserMakeAboOrder::makeOrder / makePayment Error response',
$response
);
// Bei Zahlungsfehler: Status setzen, aber next_date NICHT aktualisieren
// Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
$this->updateAboOnError($userAbo, $shoppingOrder, 3, $response);
$shoppingPayment = $userOrder->getShoppingPayment();
if ($shoppingPayment) {
$data = [
'mode' => $shoppingPayment->mode,
'txaction' => 'error',
'send_link' => false,
'payment_error' => $response,
];
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, $data);
}
} elseif ($response['status'] === 'PENDING' || $response['status'] === 'REDIRECT') {
// Pending/Redirect Status: Bestellung speichern, aber Abo nicht aktualisieren
\Log::channel('abo_order')->info('UserMakeAboOrder: Zahlung ausstehend/weiterleitung', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'status' => $response['status'],
]);
$this->info("Zahlung ausstehend für Abo {$userAbo->id}: {$response['status']}");
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: ' . $response['status']);
} else {
// Unbekannter Status: Bestellung speichern, aber Abo nicht aktualisieren
\Log::channel('abo_order')->warning('UserMakeAboOrder: Unbekannter Zahlungsstatus', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'status' => $response['status'],
]);
$this->warn("Unbekannter Zahlungsstatus für Abo {$userAbo->id}: {$response['status']}");
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: ' . $response['status']);
}
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
// Bestellung existiert (z. B. Fehler bei Payone): Abo-Fehlerstatus, Bestellung bleibt nachvollziehbar
if ($shoppingOrder) {
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
return $shoppingOrder;
}
// Noch keine ShoppingOrder (createShoppingUser / makeShoppingOrder): Exception durchreichen,
// sonst ruft der Aufrufer nur "null" ohne Ursache (z. B. Testbench, fehlende country_id im Yard).
throw $e;
}
return $shoppingOrder;
}
/**
* Aktualisiert das Abo nach einer erfolgreichen Bestellung
* Aktualisiert next_date für den nächsten Abo-Zyklus
*
* @param UserAbo $userAbo
* @param mixed $shoppingOrder
* @param int $status
* @return void
*/
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
{
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo nach erfolgreicher Zahlung', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'status' => $status,
]);
$this->info("Aktualisiere Abo: {$userAbo->id} mit Status {$status}");
try {
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
$updateData = [
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
'last_date' => now(),
];
if ($status !== 1) {
$updateData['status'] = $status;
}
$userAbo->update($updateData);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => $status,
'paid' => true,
]);
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo erfolgreich aktualisiert', [
'abo_id' => $userAbo->id,
'next_date' => $updateData['next_date'],
]);
});
// Wie bei Payment::paymentStatusPaidAction: Incentive nur wenn Callback nicht lief
// (firstOrCreate verhindert Doppelungen wenn Payone später noch trackt)
IncentiveTracker::trackAboActivated($shoppingOrder);
} catch (\Exception $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
]);
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
throw $e; // Re-throw für besseres Error-Handling
}
}
/**
* Aktualisiert das Abo bei Fehlern - OHNE next_date zu aktualisieren
* Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
*
* @param UserAbo $userAbo
* @param mixed $shoppingOrder
* @param int|string $status Status-Code oder Fehlermeldung
* @param array|null $errorResponse Optionale Fehlerantwort von Payment
* @return void
*/
private function updateAboOnError($userAbo, $shoppingOrder, $status, $errorResponse = null)
{
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo bei Fehler (ohne next_date)', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'status' => $status,
]);
$this->info("Aktualisiere Abo bei Fehler: {$userAbo->id} (Status: {$status})");
try {
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
// Nur last_date aktualisieren, next_date bleibt unverändert
// Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
$updateData = [
'last_date' => now(),
];
// Status nur setzen wenn es ein numerischer Wert ist
if (is_numeric($status)) {
$updateData['status'] = $status;
}
$userAbo->update($updateData);
// UserAboOrder mit Fehlerstatus speichern
$orderStatus = is_numeric($status) ? $status : 3; // Default zu 3 (abo_hold) wenn String
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => $orderStatus,
'paid' => false,
]);
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo bei Fehler aktualisiert (next_date unverändert)', [
'abo_id' => $userAbo->id,
'next_date' => $userAbo->next_date,
'status' => $status,
]);
});
} catch (\Exception $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos bei Fehler', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
]);
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
// Bei Fehler hier nicht re-throw, damit der Hauptprozess fortgesetzt werden kann
}
}
/**
* Berechnet die Ausführungszeit
*
* @return string
*/
private function getExecutionTime()
{
$diff = microtime(true) - $this->timeStart;
$sec = intval($diff);
$micro = $diff - $sec;
return $sec . ' Sekunden und ' . round($micro * 1000, 2) . ' ms';
}
}

View file

@ -1,135 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\UserUtil;
use App\User;
use Illuminate\Console\Command;
class UserRestore extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:restore {user_id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'User Restore: Reactivates an inactive user and restores their downline structure';
private $timeStart;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('RUN Command user:restore');
\Log::channel('cleanup')->info('COMMAND [user:restore] started.');
$this->timeStart = microtime(true);
$result = $this->restoreInactiveUsers();
\Log::channel('cleanup')->info('COMMAND [user:restore] finished.');
return $result;
}
/**
* Stellt einen deaktivierten User wieder her
* - Reaktiviert den User (setzt active=true, stellt m_sponsor wieder her)
* - Stellt die Vertriebspartner-Kinder (Downline) wieder her
* - Nutzt UserCleanUpLog um die ursprüngliche Struktur wiederherzustellen
*
* @return int
*/
private function restoreInactiveUsers()
{
$methodStartTime = microtime(true);
$this->info('START Command restoreInactiveUsers');
$user_id = $this->argument('user_id');
if (! $user_id) {
$this->error('ERROR: No user_id provided as argument');
\Log::channel('cleanup')->error('restoreInactiveUsers: No user_id provided');
return 1;
}
$this->info('Restoring user with ID: '.$user_id);
$user = User::find($user_id);
if (! $user) {
$this->error('ERROR: User not found with ID: '.$user_id);
\Log::channel('cleanup')->error('restoreInactiveUsers: User not found, user_id: '.$user_id);
return 1;
}
// Prüfe ob User bereits aktiv ist
if ($user->active) {
$this->warn('WARNING: User is already active, user_id: '.$user_id);
\Log::channel('cleanup')->warning('restoreInactiveUsers: User is already active, user_id: '.$user_id);
return 0;
}
\DB::beginTransaction();
try {
$data = [
'user_id' => $user->id,
'email' => $user->email,
'm_account' => $user->account ? $user->account->m_account : '',
'm_first_name' => $user->account ? $user->account->m_first_name : '',
'm_last_name' => $user->account ? $user->account->m_last_name : '',
];
// Reaktiviere User (setzt active=true, stellt m_sponsor aus pre_sponsor wieder her)
UserUtil::reactiveUser($user);
// Stelle alle Vertriebspartner-Kinder wieder her
UserUtil::resetChildsToSponsor($user->id);
\DB::commit();
$diff = microtime(true) - $methodStartTime;
$sec = intval($diff);
$micro = $diff - $sec;
$this->info('SUCCESS: User restored successfully');
$this->info('END Command restoreInactiveUsers | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms');
\Log::channel('cleanup')->info('restoreInactiveUsers SUCCESS: '.json_encode($data));
return 0;
} catch (\Exception $e) {
\DB::rollBack();
$this->error('ERROR: Failed to restore user: '.$e->getMessage());
\Log::channel('cleanup')->error('restoreInactiveUsers FAILED for user_id: '.$user_id.' | Error: '.$e->getMessage());
return 1;
}
}
}

37
app/Console/Kernel.php Normal file → Executable file
View file

@ -3,11 +3,6 @@
namespace App\Console;
use App\Console\Commands\BusinessStore;
use App\Console\Commands\BusinessStoreOptimized;
use App\Console\Commands\CheckPaymentsAccount;
use App\Console\Commands\DhlUpdateTracking;
use App\Console\Commands\UserCleanup;
use App\Console\Commands\UserMakeAboOrder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -20,44 +15,18 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
BusinessStore::class,
BusinessStoreOptimized::class,
CheckPaymentsAccount::class,
UserMakeAboOrder::class,
UserCleanup::class,
DhlUpdateTracking::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// Job 1: Überprüft täglich um 02:00 Uhr die Zahlungskonten.
$schedule->command('payments:check-accounts')->dailyAt('02:00');
// Jobs 2, 3, 4: Die Befehle aus deinem alten Shell-Skript.
// Werden nacheinander täglich zu unterschiedlichen Zeiten ausgeführt,
// um die Serverlast zu verteilen.
$schedule->command('business:store-optimized 0 0')->dailyAt('03:00');
$schedule->command('user:cleanup')->dailyAt('03:30');
$schedule->command('user:make_abo_order')->dailyAt('04:00');
// Abo-Chart-Snapshots: vergangene Monate einfrieren (nach allen Abo-Jobs)
$schedule->command('abo:store-chart-snapshots')->dailyAt('04:30');
// Incentive: Punkteberechnung täglich nach business:store-optimized
$schedule->command('incentive:calculate')->dailyAt('05:00');
// Cleanup old log files weekly (keeps logs for 30 days)
$schedule->command('logs:cleanup --days=30')->weekly()->sundays()->at('05:00');
// DHL Tracking Update: Stündlich mit status-basierten Intervallen und Batch-API
$schedule->command('dhl:update-tracking --days=30 --send-emails')
->hourly()
->withoutOverlapping()
->runInBackground();
// $schedule->command('inspire')
// ->hourly();
}
/**

View file

@ -1,11 +1,10 @@
<?php
namespace App\Cron;
use App\User;
use stdClass;
use App\Models\UserBusinessStructure;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\Services\BusinessPlan\TreeCalcBot;
class BusinessUsersStore
{
@ -15,7 +14,7 @@ class BusinessUsersStore
private $user_business_structure;
private $users_structure = [];
public function __construct($month, $year)
{
$this->month = $month;
@ -23,17 +22,16 @@ class BusinessUsersStore
}
public function getStoreUserBusinessStructure()
{
public function getStoreUserBusinessStructure(){
return UserBusinessStructure::where('year', $this->year)->where('month', $this->month)->first();
}
public function storeUserBusinessStructure()
{
if ($this->user_business_structure = $this->getStoreUserBusinessStructure()) {
if($this->user_business_structure = $this->getStoreUserBusinessStructure()){
return $this->user_business_structure;
}
$treeCalcBot = new TreeCalcBotOptimized($this->month, $this->year, 'admin');
$treeCalcBot = new TreeCalcBot($this->month, $this->year, 'admin');
//only load, when no structur is save
$treeCalcBot->initStructureAdmin(false);
$this->storeStructure($treeCalcBot);
@ -41,34 +39,31 @@ class BusinessUsersStore
public function storeBusinessUsersDetail()
{
if (!$this->user_business_structure) {
if(!$this->user_business_structure){
$this->user_business_structure = $this->getStoreUserBusinessStructure();
if (!$this->user_business_structure) {
if(!$this->user_business_structure){
abort(403, 'not found UserBusinessStructure');
}
}
foreach ($this->user_business_structure->users as $user_id => $completed) {
if ($completed === 0) {
$user = User::find($user_id);
if ($user) {
$TreeCalcBot = new TreeCalcBotOptimized($this->month, $this->year, 'admin');
$TreeCalcBot->initBusinesslUserDetail($user);
if (!$TreeCalcBot->business_user) {
abort(403, 'not found TreeCalcBot->business_user');
}
$this->storeBusinesslUser($TreeCalcBot->business_user);
$users = $this->user_business_structure->users;
$users[$user_id] = 1;
$this->user_business_structure->users = $users;
$this->user_business_structure->save();
foreach($this->user_business_structure->users as $user_id=>$completed){
if($completed === 0){
$user = User::findOrFail($user_id);
$TreeCalcBot = new TreeCalcBot($this->month, $this->year, 'admin');
$TreeCalcBot->initBusinesslUserDetail($user);
if(!$TreeCalcBot->business_user){
abort(403, 'not found TreeCalcBot->business_user');
}
$this->storeBusinesslUser($TreeCalcBot->business_user);
$users = $this->user_business_structure->users;
$users[$user_id] = 1;
$this->user_business_structure->users = $users;
$this->user_business_structure->save();
}
}
}
public function storeBusinesslUser($business_user)
{
public function storeBusinesslUser($business_user){
$b_user = $business_user->getBUser();
$b_user->user_items = $this->storeUserItems($business_user->businessUserItems, 1);
$b_user->b_structure_id = $this->user_business_structure->id;
@ -76,13 +71,12 @@ class BusinessUsersStore
}
public function storeBusinessCompleted()
{
if (!$this->user_business_structure) {
public function storeBusinessCompleted(){
if(!$this->user_business_structure){
$this->user_business_structure = $this->getStoreUserBusinessStructure();
}
foreach ($this->user_business_structure->users as $user_id => $completed) {
if ($completed === 0) {
foreach($this->user_business_structure->users as $user_id=>$completed){
if($completed === 0){
return false;
}
$this->user_business_structure->completed = 1;
@ -90,22 +84,21 @@ class BusinessUsersStore
}
return true;
}
private function storeUserItems($userItems, $line)
{
private function storeUserItems($userItems, $line){
$ret = [];
foreach ($userItems as $userItem) {
foreach($userItems as $userItem){
$temp = null;
if (count($userItem->businessUserItems) > 0) {
$temp = $this->storeUserItems($userItem->businessUserItems, $line + 1);
if(count($userItem->businessUserItems) > 0){
$temp = $this->storeUserItems($userItem->businessUserItems, $line+1);
}
$obj = new stdClass();
$obj->user_id = $userItem->user_id;
$obj->line = $line;
$obj->points = $userItem->sales_volume_points_sum;
$obj->parents = $temp;
$ret[] = $obj;
$ret[] = $obj;
}
return $ret;
}
@ -118,13 +111,13 @@ class BusinessUsersStore
}*/
$structure = [];
foreach ($treeCalcBot->business_users as $business_user) {
foreach($treeCalcBot->business_users as $business_user){
$structure[] = $this->storeStructureItem($business_user, 0);
}
$parentless = [];
if ($treeCalcBot->parentless) {
foreach ($treeCalcBot->parentless as $pless) {
if($treeCalcBot->parentless){
foreach($treeCalcBot->parentless as $pless){
$parentless[] = $this->storeStructureItem($pless, 0);
}
}
@ -141,15 +134,14 @@ class BusinessUsersStore
return $this->user_business_structure;
}
private function storeStructureItem($item, $deep)
{
private function storeStructureItem($item, $deep){
$temp = null;
if ($item->businessUserItems) {
foreach ($item->businessUserItems as $parent) {
$temp[] = $this->storeStructureItem($parent, $deep + 1);
}
if($item->businessUserItems){
foreach($item->businessUserItems as $parent){
$temp[] = $this->storeStructureItem($parent, $deep+1);
}
}
$this->users_structure[$item->user_id] = 0;
$obj = new stdClass();
@ -161,4 +153,7 @@ class BusinessUsersStore
$obj->parents = $temp;
return $obj;
}
}

View file

@ -1,263 +0,0 @@
<?php
namespace App\Cron;
use App\User;
use stdClass;
use App\Models\UserBusinessStructure;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use Psr\Log\LoggerInterface;
class BusinessUsersStoreOptimized
{
private $month;
private $year;
private $user_business_structure;
private $users_structure = [];
private $logger;
public function __construct($month, $year, ?LoggerInterface $logger = null)
{
$this->month = $month;
$this->year = $year;
$this->logger = $logger ?? app(LoggerInterface::class);
}
public function getStoreUserBusinessStructure()
{
return UserBusinessStructure::where('year', $this->year)
->where('month', $this->month)
->first();
}
public function storeUserBusinessStructure()
{
if ($this->user_business_structure = $this->getStoreUserBusinessStructure()) {
$this->logger->info("Found existing business structure for {$this->month}/{$this->year}");
return $this->user_business_structure;
}
try {
$this->logger->info("Creating new business structure for {$this->month}/{$this->year}");
$startTime = microtime(true);
// Verwende TreeCalcBotOptimized mit Live-Berechnung für aktuelle Daten
$treeCalcBot = new TreeCalcBotOptimized($this->month, $this->year, 'admin', true);
$treeCalcBot->initStructureAdmin(false, true); // forceLiveCalculation = true
$this->storeStructure($treeCalcBot);
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$this->logger->info("Business structure created in {$duration}ms with " . count($this->users_structure) . " users");
} catch (\Exception $e) {
$this->logger->error("Error creating business structure: " . $e->getMessage());
throw $e;
}
}
public function storeBusinessUsersDetail()
{
if (!$this->user_business_structure) {
$this->user_business_structure = $this->getStoreUserBusinessStructure();
if (!$this->user_business_structure) {
throw new \Exception('UserBusinessStructure not found');
}
}
$totalUsers = count($this->user_business_structure->users);
$processedUsers = 0;
$this->logger->info("Processing {$totalUsers} business user details");
foreach ($this->user_business_structure->users as $user_id => $completed) {
if ($completed === 0) {
try {
$user = User::find($user_id);
if ($user) {
$this->processBusinessUser($user, $user_id);
$processedUsers++;
// Log progress every 50 users
if ($processedUsers % 50 === 0) {
$this->logger->info("Processed {$processedUsers}/{$totalUsers} business users");
}
} else {
$this->logger->warning("User {$user_id} not found, skipping");
$this->markUserCompleted($user_id);
}
} catch (\Exception $e) {
$this->logger->error("Error processing user {$user_id}: " . $e->getMessage());
// Mark as completed to avoid infinite retry loops
$this->markUserCompleted($user_id);
}
}
}
$this->logger->info("Completed processing {$processedUsers} business user details");
}
private function processBusinessUser(User $user, int $user_id): void
{
try {
$startTime = microtime(true);
// Verwende TreeCalcBotOptimized für detaillierte Benutzerberechnung
$TreeCalcBot = new TreeCalcBotOptimized($this->month, $this->year, 'admin', true);
$TreeCalcBot->initBusinesslUserDetail($user, true); // forceLiveCalculation = true
if (!$TreeCalcBot->business_user) {
throw new \Exception("business_user not found for user {$user_id}");
}
$this->storeBusinesslUser($TreeCalcBot->business_user);
$this->markUserCompleted($user_id);
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$this->logger->debug("Processed user {$user_id} in {$duration}ms");
} catch (\Exception $e) {
$this->logger->error("Error in processBusinessUser for {$user_id}: " . $e->getMessage());
throw $e;
}
}
private function markUserCompleted(int $user_id): void
{
$users = $this->user_business_structure->users;
$users[$user_id] = 1;
$this->user_business_structure->users = $users;
$this->user_business_structure->save();
}
public function storeBusinesslUser($business_user)
{
try {
$b_user = $business_user->getBUser();
$b_user->user_items = $this->storeUserItems($business_user->businessUserItems, 1);
$b_user->b_structure_id = $this->user_business_structure->id;
$b_user->save();
} catch (\Exception $e) {
$this->logger->error("Error storing business user: " . $e->getMessage());
throw $e;
}
}
public function storeBusinessCompleted()
{
if (!$this->user_business_structure) {
$this->user_business_structure = $this->getStoreUserBusinessStructure();
}
$incompleteCount = 0;
foreach ($this->user_business_structure->users as $user_id => $completed) {
if ($completed === 0) {
$incompleteCount++;
}
}
if ($incompleteCount === 0) {
$this->user_business_structure->completed = 1;
$this->user_business_structure->save();
$this->logger->info("Business structure marked as completed");
return true;
}
$this->logger->info("{$incompleteCount} users still incomplete");
return false;
}
private function storeUserItems($userItems, $line)
{
$ret = [];
try {
foreach ($userItems as $userItem) {
$temp = null;
if (count($userItem->businessUserItems) > 0) {
$temp = $this->storeUserItems($userItem->businessUserItems, $line + 1);
}
$obj = new stdClass();
$obj->user_id = $userItem->user_id;
$obj->line = $line;
$obj->points = $userItem->sales_volume_points_sum ?? 0;
$obj->parents = $temp;
$ret[] = $obj;
}
} catch (\Exception $e) {
$this->logger->error("Error storing user items at line {$line}: " . $e->getMessage());
throw $e;
}
return $ret;
}
private function storeStructure($treeCalcBot)
{
try {
$structure = [];
$businessUsers = $treeCalcBot->business_users;
if (!is_array($businessUsers)) {
throw new \Exception("business_users is not an array");
}
foreach ($businessUsers as $business_user) {
$structure[] = $this->storeStructureItem($business_user, 0);
}
$parentless = [];
$parentlessUsers = $treeCalcBot->parentless;
if ($parentlessUsers && is_array($parentlessUsers)) {
foreach ($parentlessUsers as $pless) {
$parentless[] = $this->storeStructureItem($pless, 0);
}
}
$fill = [
'month' => $this->month,
'year' => $this->year,
'structure' => $structure,
'parentless' => $parentless,
'users' => $this->users_structure,
'completed' => false,
'status' => 0
];
$this->user_business_structure = UserBusinessStructure::create($fill);
$this->logger->info("Stored structure with " . count($structure) . " root users and " . count($parentless) . " parentless users");
return $this->user_business_structure;
} catch (\Exception $e) {
$this->logger->error("Error storing structure: " . $e->getMessage());
throw $e;
}
}
private function storeStructureItem($item, $deep)
{
try {
$temp = null;
if (isset($item->businessUserItems) && is_array($item->businessUserItems)) {
foreach ($item->businessUserItems as $parent) {
$temp[] = $this->storeStructureItem($parent, $deep + 1);
}
}
$this->users_structure[$item->user_id] = 0;
$obj = new stdClass();
$obj->user_id = $item->user_id;
$obj->email = $item->email ?? 'unknown';
$obj->deep = $deep;
$obj->parents = $temp;
return $obj;
} catch (\Exception $e) {
$this->logger->error("Error storing structure item for user {$item->user_id}: " . $e->getMessage());
throw $e;
}
}
}

View file

@ -1,154 +1,68 @@
<?php
namespace App\Cron;
use App\User;
use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\Services\HTMLHelper;
use App\Models\UserCreditItem;
use App\Mail\MailUserLevelUpdate;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use App\Repositories\CreditRepository;
class UserLevelUpdate
{
private $month;
private $year;
private $userLevels;
public function __construct($month, $year)
{
$this->month = $month;
$this->year = $year;
// Lade UserLevels für POS-Vergleich (wird für Prüfung benötigt)
$this->userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
}
/**
* Holt alle UserBusiness Einträge die Level-Updates benötigen
* Mit Eager Loading für bessere Performance
*/
public function getUserBusinessByMonthYear()
{
return UserBusiness::select('user_businesses.*')
->with('user') // Eager Loading für User
public function getUserBusinessByMonthYear(){
return UserBusiness::select('user_businesses.*')
->where('user_businesses.month', '=', $this->month)
->where('user_businesses.year', '=', $this->year)
->whereNotNull('user_businesses.next_qual_user_level')
->whereRaw("JSON_LENGTH(user_businesses.next_qual_user_level) > 0")
->get();
->where('user_businesses.next_qual_user_level', '!=', NULL)
->get();
}
/**
* Aktualisiert das User-Level basierend auf next_qual_user_level
* Berücksichtigt Arrays und einzelne Level-Objekte
* Prüft ob das neue Level höher ist als das aktuelle
*/
public function makeUserLevelUpdate(UserBusiness $userBusiness, $send_update_mail = false)
{
public function makeUserLevelUpdate(UserBusiness $userBusiness, $send_update_mail){
$ret = false;
if (!$userBusiness->user) {
Log::warning("UserLevelUpdate: UserBusiness {$userBusiness->id} hat kein User-Objekt");
return $ret;
}
$nextQualUserLevel = $userBusiness->next_qual_user_level;
if (!is_array($nextQualUserLevel) || empty($nextQualUserLevel)) {
return $ret;
}
// next_qual_user_level ist ein einzelnes Level-Objekt
if (is_array($nextQualUserLevel) && isset($nextQualUserLevel['id'])) {
// return wenn bereits aktualisierte Level
if (isset($nextQualUserLevel['hasUpdated']) && $nextQualUserLevel['hasUpdated'] == 1) {
return $ret;
}
$newLevelId = $nextQualUserLevel['id'];
$newLevelPos = null;
// Lade Level-Objekt für POS-Vergleich
$newLevel = $this->userLevels->get($newLevelId);
if ($newLevel) {
$newLevelPos = $newLevel->pos;
}
// Prüfe ob das neue Level höher ist als das aktuelle
$currentUserLevel = null;
if ($userBusiness->user->m_level) {
$currentUserLevel = $this->userLevels->get($userBusiness->user->m_level);
}
// Nur updaten wenn das neue Level höher ist (POS > aktuelles Level POS)
if (!$currentUserLevel || !$newLevelPos) {
return $ret;
}
if ($newLevelPos <= $currentUserLevel->pos) {
return $ret;
}
}
// Update durchführen wenn ein höheres Level gefunden wurde
try {
$userBusiness->user->m_level = $newLevel['id'];
if(!isset($nextQualUserLevel['hasUpdated']) && $userBusiness->user){
$userBusiness->user->m_level = $nextQualUserLevel['id'];
$userBusiness->user->save();
// Markiere das Level als aktualisiert in next_qual_user_level
$nextQualUserLevel['hasUpdated'] = 1;
$userBusiness->next_qual_user_level = $nextQualUserLevel;
$userBusiness->save();
$ret = $newLevelId . ' ' . ($newLevel->name ?? 'Unbekannt');
if ($send_update_mail) {
try {
$this->sendUpdateMail(
$userBusiness->user,
$userBusiness->payline_points_qual_kp ?? 0,
$newLevel->name ?? 'Unbekannt'
);
} catch (\Exception $e) {
Log::warning("UserLevelUpdate: E-Mail konnte nicht gesendet werden für User {$userBusiness->user->id}: " . $e->getMessage());
// E-Mail-Fehler sollten das Update nicht verhindern
}
$ret = $nextQualUserLevel['id'].' '.$nextQualUserLevel['name'];
if($send_update_mail){
self::sendUpdateMail($userBusiness->user, $userBusiness->total_qual_tp, $nextQualUserLevel['name']);
}
Log::info("UserLevelUpdate: User {$userBusiness->user->id} Level aktualisiert zu {$ret}");
} catch (\Exception $e) {
Log::error("UserLevelUpdate: Fehler beim Update von User {$userBusiness->user->id}: " . $e->getMessage());
throw $e;
}
}
return $ret;
}
private function sendUpdateMail(User $user, $tp, $to)
{
private function sendUpdateMail(User $user, $tp, $to){
$bcc = [];
$email = $user->email;
if (!$email) {
if ($user->mode === 'test') {
} else {
if(!$email){
if($user->mode === 'test'){
}else{
$email = config('app.checkout_mail');
}
}
if ($user->mode === 'test') {
if($user->mode === 'test'){
$bcc[] = config('app.checkout_test_mail');
} else {
}else{
$bcc[] = config('app.checkout_mail');
}
if (\App\Services\Util::isTestSystem()) {
$email = config('app.checkout_test_mail');
$bcc[] = config('app.checkout_test_mail');
}
Mail::to($email)->bcc($bcc)->locale($user->getLocale())->send(new MailUserLevelUpdate($tp, $to));
Mail::to($email)->bcc($bcc)->send(new MailUserLevelUpdate($tp, $to));
}
}

View file

@ -1,268 +0,0 @@
<?php
namespace App\Cron;
use App\Http\Controllers\Pay\PayoneController;
use App\Models\ShoppingOrder;
use App\Models\ShoppingOrderItem;
use App\Models\UserAbo;
use App\Services\AboOrderCart;
use Illuminate\Support\Facades\Log;
use Yard;
class UserMakeOrder
{
private $userAbo;
private $shopping_user;
private $shopping_order;
private $is_for;
private $user;
private $pay;
public function __construct(UserAbo $userAbo)
{
$this->userAbo = $userAbo;
Log::info('UserMakeOrder initialisiert für UserAbo ID: '.$userAbo->id);
}
public function checkProducts()
{
Log::info('Überprüfe Produkte für UserAbo ID: '.$this->userAbo->id);
$ret = [];
if (! $this->userAbo->items || $this->userAbo->items->isEmpty()) {
Log::warning('Keine Artikel für UserAbo ID: '.$this->userAbo->id.' gefunden');
return $ret;
}
// preise prüfen, ob sie sich geändert haben?
foreach ($this->userAbo->items as $item) {
$ret[] = [
'product_id' => $item->product_id,
'comp' => $item->comp,
'qty' => $item->qty,
'price' => $item->price,
'price_net' => $item->price_net,
'tax_rate' => $item->tax_rate,
'tax' => $item->tax,
'price_vk_net' => $item->price_vk_net,
'discount' => $item->discount,
'points' => $item->points,
];
}
Log::info('Produkte überprüft: '.count($ret).' Artikel gefunden');
return $ret;
}
public function makePayment($testmode = false)
{
Log::info('Starte Zahlungsvorgang für UserAbo ID: '.$this->userAbo->id);
try {
$this->pay = new PayoneController;
$this->pay->init($this->shopping_user, $this->shopping_order);
$amount = $this->shopping_order->total_shipping * 100;
// $amount = Yard::instance($this->instance)->totalWithShipping(2, '.', '') * 100;
$this->pay->setAboPayment($this->userAbo, $amount, 'EUR');
$this->pay->setPersonalData();
$response = $this->pay->onlyPaymentResponse();
\Log::info('Response: '.json_encode($response));
// $response = $this->pay->ResponseData(true);
Log::info('Zahlungsvorgang abgeschlossen für UserAbo ID: '.$this->userAbo->id.', Status: '.($response->status ?? 'unbekannt'));
return $response;
} catch (\Exception $e) {
Log::error('Fehler bei Zahlungsvorgang für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
throw $e;
}
}
public function getShoppingPayment()
{
Log::info('Rufe Zahlungsinformationen ab für UserAbo ID: '.$this->userAbo->id);
if ($this->pay) {
$payment = $this->pay->getShoppingPayment();
Log::info('Zahlungsinformationen abgerufen: '.($payment ? 'erfolgreich' : 'nicht verfügbar'));
return $payment;
}
Log::warning('Keine Zahlungsinformationen verfügbar für UserAbo ID: '.$this->userAbo->id);
return false;
}
public function createShoppingUser()
{
Log::info('Erstelle Shopping-User für UserAbo ID: '.$this->userAbo->id);
// hier muss der letzte shopping_user verwendet werden
try {
$this->shopping_user = AboOrderCart::makeCustomerDetail($this->userAbo);
$this->shopping_user->created_at = now();
$this->shopping_user->updated_at = now();
$this->shopping_user->save();
Log::info('Shopping-User erstellt für UserAbo ID: '.$this->userAbo->id.', Neue User-ID: '.$this->shopping_user->id);
return $this->shopping_user;
} catch (\Throwable $e) {
Log::error('Fehler beim Erstellen des Shopping-Users für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
throw $e;
}
}
public function makeShoppingOrder()
{
Log::info('Erstelle Bestellung für UserAbo ID: '.$this->userAbo->id);
try {
if (! $this->shopping_user) {
Log::error('Kein Shopping-User verfügbar für Bestellerstellung, UserAbo ID: '.$this->userAbo->id);
return false;
}
// WICHTIG: Yard komplett leeren vor jedem Abo, um sicherzustellen, dass keine Produkte
// aus vorherigen Abos im Cart bleiben
Yard::instance('shopping')->destroy();
// initYard akzeptiert nur einen Parameter (user_abo)
AboOrderCart::initYard($this->userAbo);
// Nochmalige Sicherheitsprüfung: Yard sollte leer sein
$yardBefore = Yard::instance('shopping');
$itemsBefore = $yardBefore->content();
if ($itemsBefore->count() > 0) {
Log::warning('UserMakeOrder: Yard war nicht leer nach initYard für Abo ID: '.$this->userAbo->id.', Items: '.$itemsBefore->count());
$yardBefore->destroy(); // Erzwinge Leerung
}
// hier wird die Bestellung erstellt inkl aktueller Preise
AboOrderCart::makeOrderYard($this->userAbo);
$yard = Yard::instance('shopping');
// Debug: Logge welche Produkte im Cart sind
$items = $yard->content();
Log::info('UserMakeOrder: Produkte im Cart nach makeOrderYard für Abo ID: '.$this->userAbo->id, [
'abo_id' => $this->userAbo->id,
'item_count' => $items->count(),
'items' => $items->map(function ($item) {
return [
'product_id' => $item->id,
'name' => $item->name,
'qty' => $item->qty,
'rowId' => $item->rowId,
];
})->toArray(),
]);
$shoppingUserStamm = $this->userAbo->shopping_user;
if (! $shoppingUserStamm) {
Log::error('UserAbo ohne shopping_user (Stammdaten-Kunde), UserAbo ID: '.$this->userAbo->id);
return false;
}
// Referenz fuer Shop/Mode: neueste Bestellung (hasOne shopping_order kann null sein z. B. wenn die
// aelteste Order soft-deleted ist oder mehrere Orders existieren).
$referenceOrder = $shoppingUserStamm->shopping_orders()
->orderByDesc('id')
->first();
if (! $referenceOrder || ! $referenceOrder->user_shop_id) {
Log::error('Fehlende Beziehungsdaten fuer Bestellerstellung (Referenz-Bestellung ohne user_shop_id), UserAbo ID: '.$this->userAbo->id, [
'shopping_user_id' => $shoppingUserStamm->id,
'reference_order_id' => $referenceOrder?->id,
]);
return false;
}
$countryId = $yard->getShippingCountryId() ?? $referenceOrder->country_id;
if (! $countryId) {
Log::error('Kein country_id (Yard shipping_country_id und Referenz-Bestellung leer), UserAbo ID: '.$this->userAbo->id, [
'yard_shipping_country_id' => $yard->getShippingCountryId(),
'reference_order_id' => $referenceOrder->id,
'reference_country_id' => $referenceOrder->country_id,
]);
return false;
}
$this->shopping_order = ShoppingOrder::create([
'shopping_user_id' => $this->shopping_user->id,
'member_id' => $this->userAbo->member_id ?? $referenceOrder->member_id,
'auth_user_id' => $this->userAbo->is_for === 'me'
? ($this->userAbo->user_id ?? $referenceOrder->auth_user_id ?? $this->shopping_user->auth_user_id)
: ($this->shopping_user->auth_user_id ?? $referenceOrder->auth_user_id),
'country_id' => $countryId,
'language' => \App::getLocale(),
'user_shop_id' => (int) $referenceOrder->user_shop_id,
'payment_for' => $this->shopping_user->getOrderPaymentFor(),
'total' => $yard->total(2, '.', ''),
'subtotal' => $yard->subtotal(2, '.', ''),
'shipping' => $yard->shipping(2, '.', ''),
'shipping_net' => $yard->shippingNet(2, '.', ''),
'subtotal_ws' => $yard->subtotalWithShipping(2, '.', ''),
'tax' => $yard->taxWithShipping(2, '.', ''),
'total_shipping' => $yard->totalWithShipping(2, '.', ''),
'points' => $yard->points(),
'weight' => $yard->weight(),
'is_abo' => 1,
'abo_interval' => $this->userAbo->abo_interval ?? 0,
'txaction' => 'prev',
'mode' => $referenceOrder->mode,
]);
Log::info('Bestellung erstellt für UserAbo ID: '.$this->userAbo->id.', Bestellnummer: '.$this->shopping_order->id);
$items = $yard->getContentByOrder();
$itemCount = 0;
foreach ($items as $item) {
if (! ShoppingOrderItem::where('shopping_order_id', $this->shopping_order->id)->where('row_id', $item->rowId)->count()) {
$price_net = $yard->rowPriceNet($item, 2, '.', '');
$tax = $item->price - $price_net;
$data = [
'shopping_order_id' => $this->shopping_order->id,
'row_id' => $item->rowId,
'product_id' => $item->id,
'comp' => $item->options->comp,
'qty' => $item->qty,
'price' => $item->price,
'price_net' => $price_net,
'tax_rate' => $item->taxRate,
'tax' => $tax,
'price_vk_net' => $this->shopping_order->getPriceVkNetBy($item->id),
'discount' => $item->options->no_commission ? 0 : $this->shopping_order->getUserDiscount(),
'points' => $item->options->points,
'slug' => $item->options->slug,
];
ShoppingOrderItem::create($data);
$itemCount++;
}
}
Log::info('Bestellpositionen hinzugefügt für UserAbo ID: '.$this->userAbo->id.', Anzahl: '.$itemCount);
$this->shopping_order->makeTaxSplit();
Log::info('Steueraufteilung für Bestellung abgeschlossen, UserAbo ID: '.$this->userAbo->id);
return $this->shopping_order;
} catch (\Throwable $e) {
Log::error('Fehler bei Bestellerstellung für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
throw $e;
}
}
}

View file

@ -25,17 +25,15 @@ class UserPaymentCredits
->where('user_businesses.month', '=', $this->month)
->where('user_businesses.year', '=', $this->year)
->where(function($q) {
return $q->where('user_businesses.commission_pp_total', '>', 0)
return $q->where('user_businesses.commission_team_total', '>', 0)
->orWhere('user_businesses.commission_shop_sales', '>', 0);
})
->get();
}
public function addUserCreditItem($userBusiness)
{
//HTMLHelper::getMonth($userBusiness->month)
$date = $userBusiness->month.'#'.$userBusiness->year;
{
$date = HTMLHelper::getMonth($userBusiness->month).' '.$userBusiness->year;
if($userBusiness->commission_shop_sales > 0){
if($this->hasNotUserCreditItem($userBusiness, 1)){
@ -43,39 +41,22 @@ class UserPaymentCredits
'user_id' => $userBusiness->user_id,
'user_business_id' => $userBusiness->id,
'credit' => $userBusiness->commission_shop_sales,
'message' => 'payment.commission_shop#'.$date,
'from_month' => $userBusiness->month,
'from_year' => $userBusiness->year,
'message' => 'Provision Shop '.$date,
'status' => 1,
]);
}
}
if($userBusiness->commission_pp_total > 0){
if($userBusiness->commission_team_total > 0){
if($this->hasNotUserCreditItem($userBusiness, 2)){
UserCreditItem::create([
'user_id' => $userBusiness->user_id,
'user_business_id' => $userBusiness->id,
'credit' => $userBusiness->commission_pp_total,
'message' => 'payment.commission_payline#'.$date,
'from_month' => $userBusiness->month,
'from_year' => $userBusiness->year,
'credit' => $userBusiness->commission_team_total,
'message' => 'Provision Team '.$date,
'status' => 2,
]);
}
}
if($userBusiness->commission_growth_total > 0){
if($this->hasNotUserCreditItem($userBusiness, 5)){
UserCreditItem::create([
'user_id' => $userBusiness->user_id,
'user_business_id' => $userBusiness->id,
'credit' => $userBusiness->commission_growth_total,
'message' => 'payment.commission_growth_bonus#'.$date,
'from_month' => $userBusiness->month,
'from_year' => $userBusiness->year,
'status' => 5,
]);
}
}
return $userBusiness;
}
@ -93,7 +74,7 @@ class UserPaymentCredits
$user = User::findOrFail($user_id);
$data = [];
if($credit_send_mail){
$data['credit_send_mail'] = false;
$data['credit_send_mail'] = true;
}
$credit_repo = new CreditRepository($user);
return $credit_repo->create($data);

View file

@ -1,277 +0,0 @@
<?php
namespace App\Domain;
/**
* Early Domain Parser Service
*
* Provides domain parsing functionality that can be used during
* bootstrap phase (RouteServiceProvider) and runtime (Middleware).
*
* This service caches parsing results per request to avoid duplicate work.
*/
class EarlyDomainParser
{
/**
* Cache for parsed domain information per request
* @var array|null
*/
private static ?array $cachedDomainInfo = null;
/**
* Cache key (host) for cache invalidation
* @var string|null
*/
private static ?string $cachedHost = null;
/**
* Parse domain information from config/domains.php
*
* Results are cached per request to avoid duplicate parsing.
*
* @param string|null $host If null, uses HTTP_HOST or SERVER_NAME
* @return array Domain information array
*/
public static function parseDomain(?string $host = null): array
{
// Get host from request if not provided
if ($host === null) {
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
}
// Remove protocol if present
$host = preg_replace('/^https?:\/\//', '', $host);
// Return cached result if available for same host
if (self::$cachedHost === $host && self::$cachedDomainInfo !== null) {
return self::$cachedDomainInfo;
}
// Load domains configuration
$domains = self::getDomainsConfig();
$reservedSubdomains = self::getReservedSubdomains();
// Check exact matches first (main, shop, crm, portal, checkout)
foreach ($domains as $key => $domainConfig) {
if ($key === 'user-shop') {
continue; // Handle user-shop separately
}
if ($host === $domainConfig['host']) {
$domainInfo = [
'type' => $domainConfig['type'],
'host' => $host,
'subdomain' => null,
'config_key' => $key,
];
// Cache the result for this request
self::$cachedHost = $host;
self::$cachedDomainInfo = $domainInfo;
return $domainInfo;
}
}
// Check for user-shop pattern (dynamic subdomains)
if (isset($domains['user-shop'])) {
$userShopPattern = $domains['user-shop']['host'];
$baseDomain = str_replace('{subdomain}.', '', $userShopPattern);
if (str_ends_with($host, '.' . $baseDomain)) {
$subdomain = str_replace('.' . $baseDomain, '', $host);
// Check if subdomain is not reserved
if (!empty($subdomain) && !in_array($subdomain, $reservedSubdomains)) {
$domainInfo = [
'type' => 'user-shop',
'host' => $host,
'subdomain' => $subdomain,
'config_key' => 'user-shop',
];
// Cache the result for this request
self::$cachedHost = $host;
self::$cachedDomainInfo = $domainInfo;
return $domainInfo;
}
}
}
// Unknown domain
$domainInfo = [
'type' => 'unknown',
'host' => $host,
'subdomain' => null,
'config_key' => null,
];
// Cache the result for this request
self::$cachedHost = $host;
self::$cachedDomainInfo = $domainInfo;
return $domainInfo;
}
/**
* Get current domain type quickly (for RouteServiceProvider)
*/
public static function getCurrentDomainType(): string
{
$domainInfo = self::parseDomain();
return $domainInfo['type'];
}
/**
* Check if current domain is a user shop
*/
public static function isUserShop(): bool
{
return self::getCurrentDomainType() === 'user-shop';
}
/**
* Get subdomain for user shops
*/
public static function getSubdomain(): ?string
{
$domainInfo = self::parseDomain();
return $domainInfo['subdomain'] ?? null;
}
/**
* Get domains configuration (early bootstrap safe)
*/
private static function getDomainsConfig(): array
{
// Try Laravel config first (if available)
if (function_exists('config')) {
$config = config('domains.domains');
if ($config) {
return $config;
}
}
// Fallback: Read config file directly
$configPath = __DIR__ . '/../../../../config/domains.php';
if (file_exists($configPath)) {
$config = include $configPath;
return $config['domains'] ?? [];
}
// Last resort: Build from environment variables
return self::buildConfigFromEnv();
}
/**
* Get reserved subdomains configuration
*/
private static function getReservedSubdomains(): array
{
// Try Laravel config first (if available)
if (function_exists('config')) {
$reserved = config('domains.reserved_subdomains');
if ($reserved) {
return $reserved;
}
}
// Fallback: Read config file directly
$configPath = __DIR__ . '/../../../../config/domains.php';
if (file_exists($configPath)) {
$config = include $configPath;
return $config['reserved_subdomains'] ?? [];
}
// Default reserved subdomains
return ['my', 'in', 'checkout', 'www', 'api', 'mail'];
}
/**
* Build basic domain configuration from environment variables
* Used as fallback when config file is not available
*/
private static function buildConfigFromEnv(): array
{
$domain = $_ENV['APP_DOMAIN'] ?? 'mivita';
$tldCare = $_ENV['APP_TLD_CARE'] ?? '.care';
$tldShop = $_ENV['APP_TLD_SHOP'] ?? '.shop';
$crmPrefix = $_ENV['APP_PRE_URL_CRM'] ?? 'my.';
$portalPrefix = $_ENV['APP_PRE_URL_PORTAL'] ?? 'in.';
$checkoutPrefix = $_ENV['APP_URL_CHECKOUT'] ?? 'checkout.';
return [
'main' => [
'host' => $domain . $tldCare,
'type' => 'main',
],
'shop' => [
'host' => $domain . $tldShop,
'type' => 'main-shop',
'default_user_shop' => 'aloevera',
],
'crm' => [
'host' => $crmPrefix . $domain . $tldCare,
'type' => 'crm',
],
'portal' => [
'host' => $portalPrefix . $domain . $tldCare,
'type' => 'portal',
],
'checkout' => [
'host' => $checkoutPrefix . $domain . $tldCare,
'type' => 'checkout',
],
'user-shop' => [
'host' => '{subdomain}.' . $domain . $tldCare,
'type' => 'user-shop',
],
];
}
/**
* Get protocol from configuration
*/
public static function getProtocol(): string
{
if (function_exists('config')) {
return config('domains.protocol', 'https://');
}
return $_ENV['APP_PROTOCOL'] ?? 'https://';
}
/**
* Get main domain URL for redirects
*/
public static function getMainUrl(): string
{
$domains = self::getDomainsConfig();
$mainHost = $domains['main']['host'] ?? 'localhost';
$protocol = self::getProtocol();
return $protocol . $mainHost;
}
/**
* Clear the internal cache (useful for testing or special cases)
*/
public static function clearCache(): void
{
self::$cachedDomainInfo = null;
self::$cachedHost = null;
}
/**
* Check if result is cached for current/given host
*/
public static function isCached(?string $host = null): bool
{
if ($host === null) {
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
$host = preg_replace('/^https?:\/\//', '', $host);
}
return self::$cachedHost === $host && self::$cachedDomainInfo !== null;
}
}

71
app/Exceptions/Handler.php Normal file → Executable file
View file

@ -2,12 +2,8 @@
namespace App\Exceptions;
use Throwable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Throwable;
class Handler extends ExceptionHandler
{
@ -40,10 +36,6 @@ class Handler extends ExceptionHandler
*/
public function report(Throwable $exception)
{
if ($this->shouldReport($exception)) {
$this->sendEmail($exception);
}
parent::report($exception);
}
@ -60,65 +52,4 @@ class Handler extends ExceptionHandler
{
return parent::render($request, $exception);
}
/**
* Convert an authentication exception into a response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function unauthenticated($request, \Illuminate\Auth\AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 401);
}
try {
// HOTFIX: DomainContext temporär deaktiviert
// TODO: Nach Claude v2 Implementation wieder aktivieren
// $context = app(\App\Domain\DomainContext::class);
// $loginRoute = match($context->type) {
// 'portal' => 'portal.login.form',
// 'crm' => 'login', // CRM hat eine eigene login route
// default => 'login'
// };
// Temporär: Verwende Standard-Login-Route
return redirect()->guest(route('login'));
} catch (\Exception $e) {
// Fallback: Weiterleitung zur Hauptdomain
return redirect()->guest('https://' . config('app.domain') . config('app.tld_care') . '/login');
}
}
public function sendEmail(Throwable $exception)
{
try {
$e = FlattenException::create($exception);
$handler = new HtmlErrorRenderer(true); // boolean, true raises debug flag...
$css = $handler->getStylesheet();
$content = $handler->getBody($e);
//Mail::to(config('app.exception_mail'))->send(new MailContact($contact));
// Verwende normale Mail-Klasse statt Facade, um Probleme bei der Initialisierung zu vermeiden
$to = config('app.exception_mail');
$subject = 'mivita Exception: ' . \Request::fullUrl();
if ($to) {
\Mail::send('emails.exception', compact('css', 'content'), function ($message) use ($to, $subject) {
$message
->to($to)
->subject($subject)
;
});
}
} catch (Throwable $ex) {
// Einfache Fehlerprotokollierung ohne Facade
file_put_contents(
storage_path('logs/laravel-' . date('Y-m-d') . '.log'),
'[' . date('Y-m-d H:i:s') . '] exception-handler-error: ' . $ex->getMessage() . "\n",
FILE_APPEND
);
}
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Excel;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithHeadings;
class UserTeamExport implements FromCollection, WithHeadings
{
protected $collection;
protected $headings;
use Exportable;
public function __construct($data,$header)
{
$this->collection = $data;
$this->headings = $header;
}
public function collection()
{
return collect($this->collection);
}
public function headings(): array
{
return [$this->headings];
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Excel;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithHeadings;
class xExport implements FromCollection, WithHeadings
{
protected $collection;
protected $headings;
use Exportable;
public function __construct($data,$header)
{
$this->collection = $data;
$this->headings = $header;
}
public function collection()
{
return collect($this->collection);
}
public function headings(): array
{
return [$this->headings];
}
}

View file

@ -1,174 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UserAbo;
use App\Repositories\AboRepository;
use App\Services\AboItemHistoryService;
use App\Services\AboOrderCart;
use App\Services\Shop;
use Request;
class AboController extends Controller
{
protected $aboRepository;
public function __construct(AboRepository $aboRepository)
{
$this->middleware('admin');
$this->aboRepository = $aboRepository;
}
public function index()
{
if (Request::get('reset') === 'filter') {
set_user_attr('filter_user_shop_id', null);
set_user_attr('filter_status', null);
set_user_attr('filter_member_id', null);
return redirect(route('admin_sales_customers'));
}
// $filter_user_shops = UserAbo::join('user_shops', 'user_shop_id', '=', 'user_shops.id')->orderBy('slug')->get()->pluck('slug', 'id')->unique()->toArray();
$filter_members = UserAbo::join('users', 'user_id', '=', 'users.id')->groupBy('user_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get();
$data = [
// 'filter_user_shops' => $filter_user_shops,
'filter_members' => $filter_members,
];
return view('admin.abo.index', $data);
}
public function detail($id)
{
$data = Request::all();
$user_abo = UserAbo::findOrFail($id);
// init Yard
AboOrderCart::initYard($user_abo);
$customer_detail = AboOrderCart::getCustomerDetail();
AboOrderCart::makeOrderYard($user_abo);
$comp_products = [];
if ($user_abo->is_for === 'me') {
$comp_products = Shop::getCompProducts('abo-me');
}
$data = [
'user_abo' => $user_abo,
'isAdmin' => true,
'customer_detail' => $customer_detail,
'view' => $user_abo->is_for,
'comp_products' => $comp_products,
];
return view('admin.abo.detail', $data);
}
public function update($id)
{
$data = Request::all();
if (isset($data['action'])) {
if ($data['action'] === 'abo_update_settings') {
$user_abo = UserAbo::findOrFail($data['id']);
$this->aboRepository->setModel($user_abo);
$this->aboRepository->update($data);
return redirect(route('admin_abos_detail', [$id]));
}
}
}
public function rollback($id)
{
$user_abo = UserAbo::findOrFail($id);
AboOrderCart::initYard($user_abo);
$success = AboItemHistoryService::rollbackToInitial($user_abo);
if ($success) {
$user_abo->refresh();
AboOrderCart::makeOrderYard($user_abo);
AboOrderCart::checkNumOfCompProducts($user_abo);
\Session()->flash('alert-success', __('abo_history.rollback_success'));
} else {
\Session()->flash('alert-error', __('abo_history.rollback_no_data'));
}
return redirect(route('admin_abos_detail', [$id]));
}
public function datatable()
{
$query = UserAbo::with('user_abo_orders')->with('shopping_user')->select('user_abos.*');
set_user_attr('filter_member_id', Request::get('filter_member_id'));
if (Request::get('filter_member_id') != '') {
$query->where('user_id', '=', Request::get('filter_member_id'));
}
set_user_attr('filter_status', Request::get('filter_status'));
if (Request::get('filter_status') != '') {
$query->where('status', '=', Request::get('filter_status'));
}
return \DataTables::eloquent($query)
->addColumn('id', function (UserAbo $user_abo) {
return '<a href="'.route('admin_abos_detail', [$user_abo->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('start_date', function (UserAbo $user_abo) {
return $user_abo->start_date;
})
->addColumn('next_date', function (UserAbo $user_abo) {
return $user_abo->next_date;
})
->addColumn('abo_interval', function (UserAbo $user_abo) {
return \App\Services\HTMLHelper::getAboStrLang($user_abo->abo_interval);
})
->addColumn('status', function (UserAbo $user_abo) {
return $user_abo->getStatusFormated();
})
->addColumn('active', function (UserAbo $user_abo) {
return get_active_badge($user_abo->active);
})
->addColumn('is_for', function (UserAbo $user_abo) {
return $user_abo->getIsForFormated();
})
->addColumn('count', function (UserAbo $user_abo) {
return $user_abo->getCountOrders();
})
->addColumn('amount', function (UserAbo $user_abo) {
return $user_abo->getFormattedAmount().' €';
})
->addColumn('payment', function (UserAbo $user_abo) {
return $user_abo->getPaymentType();
})
->addColumn('member', function (UserAbo $user_abo) {
if (isset($user_abo->shopping_user) && $user_abo->shopping_user->member_id > 0) {
return '<a href="'.route('admin_lead_edit', [$user_abo->shopping_user->member_id]).'">'.$user_abo->shopping_user->member->getFullName().'</a>';
}
})
->addColumn('payone_userid', function (UserAbo $user_abo) {
return $user_abo->payone_userid;
})
->orderColumn('id', 'id $1')
->orderColumn('start_date', 'start_date $1')
->orderColumn('next_date', 'next_date $1')
->orderColumn('abo_interval', 'abo_interval $1')
->orderColumn('status', 'status $1')
->orderColumn('active', 'active $1')
->orderColumn('is_for', 'is_for $1')
->orderColumn('count', 'count $1')
->orderColumn('amount', 'amount $1')
->orderColumn('payone_userid', 'payone_userid $1')
->rawColumns(['id', 'status', 'active', 'is_for', 'member'])
->make(true);
}
}

View file

@ -1,207 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DcCategory;
use App\Models\DcFile;
use App\Models\DcTag;
use App\Repositories\DC\FileRepository;
use App\Repositories\DC\TagRepository;
use Request;
use Response;
use Util;
class DownloadController extends Controller
{
protected $tagRepository;
protected $fileRepository;
public function __construct(TagRepository $tagRepository, FileRepository $fileRepository)
{
$this->middleware('admin');
$this->tagRepository = $tagRepository;
$this->fileRepository = $fileRepository;
}
public function files()
{
$q = DcFile::orderBy('id', 'desc')->get(); // File::all();
$data = [
'files' => $q,
];
return view('admin.downloadcenter.files', $data);
}
public function fileEdit($id = null)
{
$file = $id ? DcFile::find($id) : new DcFile;
$data = [
'file' => $file,
'categories' => DcCategory::where('active', true)->orderBy('pos')->get(),
'tags' => DcTag::orderBy('pos')->get(),
];
return view('admin.downloadcenter.file_edit', $data);
}
public function fileUpdate($do, $id)
{
if ($do === 'make_thumb') {
$this->fileRepository->makeThumb($id);
\Session()->flash('alert-success', 'Vorschaubild erstellt!');
return back();
}
if ($do === 'delete') {
$this->fileRepository->deleteFile($id);
\Session()->flash('alert-success', 'Datei gelöscht!');
return redirect(route('admin_downloadcenter_files'));
}
if ($do === 'delete_thumb') {
$this->fileRepository->deleteThumb($id);
\Session()->flash('alert-success', 'Vorschaubild gelöscht!');
return back();
}
if ($do === 'deactivate') {
$file = DcFile::findOrFail($id);
$file->active = false;
$file->save();
\Session()->flash('alert-success', 'Datei nicht anzeigen!');
return back();
}
if ($do === 'activate') {
$file = DcFile::findOrFail($id);
$file->active = true;
$file->save();
\Session()->flash('alert-success', 'Datei wird angezeigt!');
return back();
}
if ($do === 'file_tags_update') {
$file = DcFile::findOrFail($id);
$tags = Request::get('nestable_check', []);
$this->fileRepository->tagsUpdate($id, is_array($tags) ? $tags : []);
\Session()->flash('alert-success', 'Tags aktualisiert!');
return back();
}
}
public function upload()
{
return view('admin.downloadcenter.file_upload');
}
public function uploadFile()
{
$data = Request::all();
$file = $this->fileRepository->uploadFile($data);
return Response::json([
'error' => false,
'filename' => $file->filename,
'filedata' => '',
'code' => 200,
], 200);
// return response()->json(['success'=>basename($file)]);
}
public function tags($flash = false)
{
$active = DcCategory::orderBy('pos')->get();
$inactive = DcTag::where('category_id', null)->get();
$data = [
'category_active' => $active,
'tags_inactive' => $inactive,
];
if ($flash) {
\Session()->flash('alert-success', 'gespeichert!');
}
return view('admin.downloadcenter.tags', $data);
}
public function storeItem($obj = false)
{
$data = Request::all();
return $this->tagRepository->storeItem($obj, $data);
return redirect(route('admin_downloadcenter_tags'));
}
public function deleteItem($obj, $id)
{
$this->tagRepository->deleteItem($obj, $id);
return redirect(route('admin_downloadcenter_tags'));
}
public function datatable()
{
$query = DcFile::with('tags')->select('dc_files.*');
return \DataTables::eloquent($query)
->addColumn('id', function (DcFile $file) {
return '<a href="'.route('admin_downloadcenter_file_edit', [$file->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('image', function (DcFile $file) {
return ($file->hasThumb() && $file->hasBig()) ?
'<img src="'.route('storage_file', [$file->id, 'dc_thumb', 'image']).'" class="img-fluid img-responsive" style="max-width: 100px;">' :
'<a href="'.route('admin_downloadcenter_file', ['make_thumb', $file->id]).'" class="btn btn-sm btn-warning"> Vorschaubild<br>erstellen <i class="ion ion-md-refresh-circle"></i></a>';
})
->addColumn('name', function (DcFile $file) {
// Storage::disk('local')->url($file->filename) }}
return '<a target="_blank" href="'.route('storage_file', [$file->id, 'dc_file', 'stream']).'">'.$file->original_name.'</a>';
// return '<a target="_blank" href="">'.$file->original_name.'</a>';
})
->addColumn('category', function (DcFile $file) {
// return $file->category ? $file->category->name : '';
})
->addColumn('tags', function (DcFile $file) {
// return $file->hasTags() ? '<span class="badge badge-pill badge-success">('.$file->fileTag()->count().')</span>' : '<span class="badge badge-pill badge-dange">X</span>';
return $file->tags->implode('name', '<br>');
})
->addColumn('size', function (DcFile $file) {
return Util::formatBytes($file->size);
})
->addColumn('active', function (DcFile $file) {
return get_active_badge($file->active);
// return $file->active ? '<span class="badge badge-pill badge-success"><i class="fa fa-check-circle"></i> aktiv</span>' : '<span class="badge badge-pill badge-danger"><i class="fa fa-times-circle"></i> inaktiv</span>';
})
->addColumn('created_at', function (DcFile $file) {
return $file->created_at->format('d.m.Y H:i');
})
->addColumn('updated_at', function (DcFile $file) {
return $file->updated_at->format('d.m.Y');
})
->addColumn('action', function (DcFile $file) {
return '<a onclick="return confirm(\'Diese Datei wirklich löschen?\');" class="btn btn-sm btn-danger" href="'.route('admin_downloadcenter_file', ['delete', $file->id]).'"><i class="fa fa-trash"></i></a>';
})
->filterColumn('name', function ($query, $keyword) {
if ($keyword != '') {
$query->where('original_name', 'LIKE', '%'.$keyword.'%');
}
})
->orderColumn('id', 'id $1')
->orderColumn('name', 'original_name $1')
->orderColumn('original_name', 'original_name $1')
->orderColumn('category', 'category $1')
->orderColumn('size', 'size $1')
->orderColumn('active', 'active $1')
->orderColumn('created_at', 'created_at $1')
->rawColumns(['id', 'image', 'name', 'active', 'tags', 'action'])
->make(true);
}
}

View file

@ -1,411 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveNewPartner;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Services\Incentive\IncentiveCalculationService;
use App\User;
use Request;
class IncentiveController extends Controller
{
public function __construct()
{
$this->middleware('admin');
}
public function index()
{
return view('admin.incentive.index');
}
public function create()
{
return view('admin.incentive.create', [
'languages' => config('localization.supportedLocales'),
]);
}
public function store()
{
$data = Request::validate([
'name' => 'required|string|max:255',
'subtitle' => 'nullable|string|max:255',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'terms' => 'nullable|string',
'qualification_start' => 'required|date',
'qualification_end' => 'required|date|after_or_equal:qualification_start',
'calculation_end' => 'required|date|after_or_equal:qualification_end',
'points_partner_onetime' => 'required|integer|min:0',
'points_abo_onetime' => 'required|integer|min:0',
'min_direct_partners' => 'required|integer|min:0',
'min_customer_abos' => 'required|integer|min:0',
'max_winners' => 'required|integer|min:1',
'status' => 'required|integer|in:0,1,2',
]);
$data = array_merge($data, $this->extractTranslations());
Incentive::create($data);
\Session()->flash('alert-success', __('incentive.created'));
return redirect(route('admin_incentives'));
}
public function show($id)
{
$incentive = Incentive::findOrFail($id);
$participants = IncentiveParticipant::where('incentive_id', $incentive->id)
->with('user', 'user.account')
->orderByIncentiveLeaderboard()
->get();
return view('admin.incentive.show', [
'incentive' => $incentive,
'participants' => $participants,
]);
}
public function edit($id)
{
$incentive = Incentive::findOrFail($id);
return view('admin.incentive.edit', [
'incentive' => $incentive,
'languages' => config('localization.supportedLocales'),
]);
}
public function update($id)
{
$data = Request::validate([
'name' => 'required|string|max:255',
'subtitle' => 'nullable|string|max:255',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'terms' => 'nullable|string',
'qualification_start' => 'required|date',
'qualification_end' => 'required|date|after_or_equal:qualification_start',
'calculation_end' => 'required|date|after_or_equal:qualification_end',
'points_partner_onetime' => 'required|integer|min:0',
'points_abo_onetime' => 'required|integer|min:0',
'min_direct_partners' => 'required|integer|min:0',
'min_customer_abos' => 'required|integer|min:0',
'max_winners' => 'required|integer|min:1',
'status' => 'required|integer|in:0,1,2',
]);
$data = array_merge($data, $this->extractTranslations());
$incentive = Incentive::findOrFail($id);
$incentive->update($data);
\Session()->flash('alert-success', __('incentive.updated'));
return redirect(route('admin_incentive_show', [$id]));
}
/**
* @return array{trans_name: array<string, string>, trans_description: array<string, string>, trans_terms: array<string, string>}
*/
private function extractTranslations(): array
{
$transName = [];
$transDescription = [];
$transTerms = [];
$transSubtitle = [];
foreach (config('localization.supportedLocales') as $locale => $localeData) {
if ($locale !== 'de') {
$transName[$locale] = Request::get('trans_name_'.$locale, '');
$transSubtitle[$locale] = Request::get('trans_subtitle_'.$locale, '');
$transDescription[$locale] = Request::get('trans_description_'.$locale, '');
$transTerms[$locale] = Request::get('trans_terms_'.$locale, '');
}
}
return [
'trans_name' => $transName,
'trans_subtitle' => $transSubtitle,
'trans_description' => $transDescription,
'trans_terms' => $transTerms,
];
}
public function recalculate($id)
{
$incentive = Incentive::findOrFail($id);
$service = new IncentiveCalculationService;
$stats = $service->recalculate($incentive, Request::has('force'));
\Session()->flash('alert-success', __('incentive.recalculated', [
'participants' => $stats['participants'],
'errors' => $stats['errors'],
]));
return redirect(route('admin_incentive_show', [$id]));
}
public function participantDetails($participant_id)
{
$participant = IncentiveParticipant::with('incentive', 'user', 'user.account')
->findOrFail($participant_id);
$data = self::buildParticipantDetailData($participant);
return view('admin.incentive._participant_details', $data);
}
/**
* Baut die Detail-Daten fuer einen Teilnehmer auf.
* Wird von Admin und User Controller genutzt.
*/
public static function buildParticipantDetailData(IncentiveParticipant $participant): array
{
$incentive = $participant->incentive;
$calculation_months = $incentive->getCalculationMonths();
// Alle Logs dieses Teilnehmers (ohne Stornos)
$all_logs = IncentivePointsLog::where('participant_id', $participant->id)
->where('is_storno', false)
->with('salesVolume')
->orderBy('created_at')
->get();
// UserSalesVolume-IDs -> User-ID Mapping aufbauen (fuer akkumulierte Partner-Punkte)
$sv_ids = $all_logs->whereNotNull('user_sales_volume_id')->pluck('user_sales_volume_id')->unique()->toArray();
$sv_user_map = ! empty($sv_ids)
? UserSalesVolume::whereIn('id', $sv_ids)->pluck('user_id', 'id')->toArray()
: [];
// Partner aus Tracking-Tabelle
$new_partners = IncentiveNewPartner::where('participant_id', $participant->id)
->with('user', 'user.account')
->orderBy('registered_at')
->get();
$partner_logs = $all_logs->where('type', 'partner');
$partner_sources = $new_partners->map(function ($np) use ($partner_logs, $sv_user_map, $calculation_months, $incentive) {
$monthly = [];
$transactions = [];
foreach ($calculation_months as $period) {
// Akkumulierte Logs: source_type=UserSalesVolume, deren USV.user_id == partner user_id
$month_logs = $partner_logs
->where('month', $period['month'])
->where('year', $period['year'])
->filter(function ($log) use ($np, $sv_user_map) {
if ($log->source_type === User::class) {
return false; // Einmal-Punkte nicht in Monatsspalte
}
if ($log->incentive_new_partner_id) {
return (int) $log->incentive_new_partner_id === (int) $np->id;
}
// Legacy: USV.user_id muss zum Partner gehoeren
return isset($sv_user_map[$log->user_sales_volume_id])
&& $sv_user_map[$log->user_sales_volume_id] === $np->user_id;
});
$month_points = (int) $month_logs->sum('points_accumulated');
$monthly[] = $month_points;
foreach ($month_logs as $log) {
$transactions[] = [
'date' => $log->created_at->format('d.m.Y'),
'month' => $period['month'],
'year' => $period['year'],
'label' => $log->source_label ?: 'KP #'.($log->user_sales_volume_id ?? $log->source_id),
'points' => $log->points_accumulated,
'type' => 'accumulated',
];
}
}
// Einmal-Punkte als Transaktion hinzufuegen
$onetime_log = $partner_logs
->where('source_type', User::class)
->first(function ($log) use ($np) {
if ($log->incentive_new_partner_id) {
return (int) $log->incentive_new_partner_id === (int) $np->id;
}
return (int) $log->source_id === (int) $np->user_id;
});
if ($onetime_log) {
array_unshift($transactions, [
'date' => $onetime_log->created_at->format('d.m.Y'),
'month' => $onetime_log->month,
'year' => $onetime_log->year,
'label' => __('incentive.onetime_registration'),
'points' => $onetime_log->points_onetime,
'type' => 'onetime',
]);
}
return [
'id' => $np->id,
'label' => $np->user ? ($np->user->getFullName() ?: $np->user->email ?: 'User #'.$np->user_id) : 'User #'.$np->user_id,
'month' => $np->registered_at->month,
'year' => $np->registered_at->year,
'onetime' => $incentive->points_partner_onetime,
'monthly' => $monthly,
'total' => $incentive->points_partner_onetime + array_sum($monthly),
'transactions' => $transactions,
];
});
// Abos aus Tracking-Tabelle
$new_abos = IncentiveNewAbo::where('participant_id', $participant->id)
->with('userAbo')
->orderBy('activated_at')
->get();
$abo_logs = $all_logs->where('type', 'abo');
// Legacy-Fallback: USV -> user_abo_id (Logs ohne incentive_new_abo_id)
$sv_user_abo_map = [];
$needs_legacy_abo_map = $abo_logs->whereNull('incentive_new_abo_id')->whereNotNull('user_sales_volume_id')->isNotEmpty();
if ($needs_legacy_abo_map && ! empty($sv_ids)) {
$sv_rows = UserSalesVolume::query()
->whereIn('id', $sv_ids)
->whereNotNull('shopping_order_id')
->get(['id', 'shopping_order_id']);
$order_ids = $sv_rows->pluck('shopping_order_id')->unique()->filter()->values();
if ($order_ids->isNotEmpty()) {
$user_abo_id_by_order_id = UserAboOrder::query()
->whereIn('shopping_order_id', $order_ids)
->get(['shopping_order_id', 'user_abo_id'])
->keyBy('shopping_order_id');
foreach ($sv_rows as $sv) {
$link = $user_abo_id_by_order_id->get($sv->shopping_order_id);
if ($link) {
$sv_user_abo_map[$sv->id] = (int) $link->user_abo_id;
}
}
}
}
$abo_sources = $new_abos->map(function ($na) use ($abo_logs, $sv_user_abo_map, $calculation_months, $incentive) {
$monthly = [];
$transactions = [];
$tracked_user_abo_id = (int) $na->user_abo_id;
foreach ($calculation_months as $period) {
$month_logs = $abo_logs
->where('month', $period['month'])
->where('year', $period['year'])
->filter(function ($log) use ($na, $tracked_user_abo_id, $sv_user_abo_map) {
if ($log->source_type !== UserSalesVolume::class) {
return false;
}
if ($log->incentive_new_abo_id) {
return (int) $log->incentive_new_abo_id === (int) $na->id;
}
$usv_id = $log->user_sales_volume_id;
if (! $usv_id || ! isset($sv_user_abo_map[$usv_id])) {
return false;
}
return $sv_user_abo_map[$usv_id] === $tracked_user_abo_id;
});
$month_points = (int) $month_logs->sum('points_accumulated');
$monthly[] = $month_points;
foreach ($month_logs as $log) {
$transactions[] = [
'date' => $log->created_at->format('d.m.Y'),
'month' => $period['month'],
'year' => $period['year'],
'label' => $log->source_label ?: 'SV #'.($log->user_sales_volume_id ?? $log->source_id),
'points' => $log->points_accumulated,
'type' => 'accumulated',
];
}
}
// Einmal-Punkte als Transaktion
$onetime_log = $abo_logs
->where('source_type', '!=', UserSalesVolume::class)
->first(function ($log) use ($na) {
if ($log->incentive_new_abo_id) {
return (int) $log->incentive_new_abo_id === (int) $na->id;
}
return (int) $log->source_id === (int) $na->user_abo_id;
});
if ($onetime_log) {
array_unshift($transactions, [
'date' => $onetime_log->created_at->format('d.m.Y'),
'month' => $onetime_log->month,
'year' => $onetime_log->year,
'label' => __('incentive.onetime_abo_activation'),
'points' => $onetime_log->points_onetime,
'type' => 'onetime',
]);
}
$label = $na->userAbo?->email ?: ('Abo #'.$na->user_abo_id);
return [
'id' => $na->id,
'label' => $label,
'month' => $na->activated_at->month,
'year' => $na->activated_at->year,
'onetime' => $incentive->points_abo_onetime,
'monthly' => $monthly,
'total' => $incentive->points_abo_onetime + array_sum($monthly),
'transactions' => $transactions,
];
});
return [
'incentive' => $incentive,
'participant' => $participant,
'calculation_months' => $calculation_months,
'partner_sources' => $partner_sources,
'abo_sources' => $abo_sources,
];
}
public function datatable()
{
$query = Incentive::query()->select('incentives.*');
return \DataTables::eloquent($query)
->addColumn('action', function (Incentive $incentive) {
return '<a href="'.route('admin_incentive_show', [$incentive->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-eye"></span></a>
<a href="'.route('admin_incentive_edit', [$incentive->id]).'" class="btn icon-btn btn-sm btn-warning"><span class="fa fa-edit"></span></a>';
})
->addColumn('status_label', function (Incentive $incentive) {
return '<span class="badge badge-'.$incentive->getStatusColor().'">'.$incentive->getStatusType().'</span>';
})
->addColumn('period', function (Incentive $incentive) {
return $incentive->qualification_start->format('d.m.Y').' - '.$incentive->qualification_end->format('d.m.Y');
})
->addColumn('participants_count', function (Incentive $incentive) {
return $incentive->participants()->count();
})
->orderColumn('name', 'name $1')
->orderColumn('status_label', 'status $1')
->rawColumns(['action', 'status_label'])
->make(true);
}
}

View file

@ -1,467 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Auth;
use Request;
use App\User;
use Carbon\Carbon;
use App\Exports\xExport;
use App\Models\UserInvoice;
use App\Services\HTMLHelper;
use App\Models\ShoppingOrder;
use App\Exports\UserTeamExport;
use App\Models\ShoppingOrderItem;
use App\Http\Controllers\Controller;
use Maatwebsite\Excel\Facades\Excel;
use App\Services\BusinessPlan\ExportBot;
use Illuminate\Database\Eloquent\Collection;
class PaymentSalesController extends Controller
{
public function __construct()
{
$this->middleware('admin');
}
public function index()
{
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_years' => HTMLHelper::getYearRange(2022),
];
return view('admin.payment.salesvolume', $data);
}
public function download(){
/*
EXCEL EXPORT function */
/*
if(Request::get('action') === "exportfull_paid"){
return $this->exportFullList(1);
}
if(Request::get('action') === "exportfull_unpaid"){
return $this->exportFullList(0);
}
*/
if(Request::get('action') === "exportfull_paid_invoice"){
return $this->exportFullListInvoice();
}
if(Request::get('action') === "export"){
return $this->exportKompaktListInvoice();
}
}
private function setFilterVars(){
if(!session('payment_sales_vol_filter_month')){
session(['payment_sales_vol_filter_month' => intval(date('m'))]);
}
if(!session('payment_sales_vol_filter_year')){
session(['payment_sales_vol_filter_year' => intval(date('Y'))]);
}
if(Request::get('payment_sales_vol_filter_month')){
session(['payment_sales_vol_filter_month' => Request::get('payment_sales_vol_filter_month')]);
}
if(Request::get('payment_sales_vol_filter_year')){
session(['payment_sales_vol_filter_year' => Request::get('payment_sales_vol_filter_year')]);
}
}
private function exportKompaktListInvoice(){
$objects = $this->initKompaktList();
$columns = [];
$filename = "mivita-absatzmengen-kompakt".session('payment_sales_vol_filter_month').'_'.session('payment_sales_vol_filter_year')."-export";
$headers = array(
'#',
'Produkt',
'Artikelnummer',
'Menge',
);
if($objects){
foreach ($objects as $key => $obj){
$columns[] = array(
'id' => $key,
'name' => $obj['name'],
'number' => $obj['number'],
'value' => $obj['value'],
);
}
}
return Excel::download(new UserTeamExport($columns, $headers), $filename.'.xls');
}
private function exportFullListInvoice(){
$this->setFilterVars();
$UserInvoices = UserInvoice::with('shopping_order')->with('shopping_order.shopping_user')->select('user_invoices.*')
->where('user_invoices.month', '=', session('payment_sales_vol_filter_month'))
->where('user_invoices.year', '=', session('payment_sales_vol_filter_year'))
->get();
$headers = array('Rechnungsnummer','Datum', 'EMail', 'Zahlung', 'ProduktNummer', 'ProduktName', 'Anzahl', 'Summe', 'Kompensation');
$columns = [];
$hasSOID = [];
$total_value = [];
foreach($UserInvoices as $UserInvoice){
if($UserInvoice->shopping_order){
$ShoppingOrder = $UserInvoice->shopping_order;
$object = [];
$object['Rechnungsnummer'] = $UserInvoice->full_number;
$object['Datum'] = $UserInvoice->date;
$object['EMail'] = $ShoppingOrder->shopping_user ? $ShoppingOrder->shopping_user->billing_email : 'n/a';
$object['Zahlung'] = $ShoppingOrder->getPaymentForType();
if($ShoppingOrder->payment_for === 5){ //homeparty
if($ShoppingOrder->homeparty){
foreach($ShoppingOrder->homeparty->homeparty_order_items as $homeparty_item){
$total_value[$homeparty_item->product_id] = isset($total_value[$homeparty_item->product_id]) ? $total_value[$homeparty_item->product_id] + $homeparty_item->qty : $homeparty_item->qty;
$object['ProduktNummer'] = $homeparty_item->product ? $homeparty_item->product->number : "n/a";
$object['ProduktName'] = $homeparty_item->product ? $homeparty_item->product->name : "n/a";
$object['Anzahl'] = $homeparty_item->qty;
$object['Summe'] = $total_value[$homeparty_item->product_id];
$object['Kompensation'] = '';
$columns[] = $object;
}
}
}elseif($ShoppingOrder->payment_for === 8){ //collective_invoice
if($ShoppingOrder->shopping_collect_order){
foreach($ShoppingOrder->shopping_collect_order->shop_items as $shop_item){
$total_value[$shop_item['pid']] = isset($total_value[$shop_item['pid']]) ? $total_value[$shop_item['pid']] + $shop_item['qty'] : $shop_item['qty'];
$object['ProduktNummer'] = $shop_item['number'];
$object['ProduktName'] = $shop_item['name'];
$object['Anzahl'] = $shop_item['qty'];
$object['Summe'] = $total_value[$shop_item['pid']];
$object['Kompensation'] = '';
$columns[] = $object;
}
}
}else{
if($ShoppingOrder->shopping_order_items){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
$total_value[$shopping_order_item->product_id] = isset($total_value[$shopping_order_item->product_id]) ? $total_value[$shopping_order_item->product_id] + $shopping_order_item->qty : $shopping_order_item->qty;
$object['ProduktNummer'] = $shopping_order_item->product ? $shopping_order_item->product->number : "n/a";
$object['ProduktName'] = $shopping_order_item->product ? $shopping_order_item->product->name : "n/a";
$object['Anzahl'] = $shopping_order_item->qty;
$object['Summe'] = $total_value[$shopping_order_item->product_id];
$object['Kompensation'] = ($shopping_order_item->comp ? $shopping_order_item->comp : '');
$columns[] = $object;
}
}
}
$hasSOID[] = $ShoppingOrder->id;
}
}
$filename = "mivita-absatzmengen-voll-".session('payment_sales_vol_filter_month').'_'.session('payment_sales_vol_filter_year')."-export";
return Excel::download(new xExport($columns, $headers), $filename.'.xls');
}
private function initKompaktList()
{
$this->setFilterVars();
$UserInvoices = UserInvoice::with('shopping_order')->with('shopping_order.shopping_user')->select('user_invoices.*')
->where('user_invoices.month', '=', session('payment_sales_vol_filter_month'))
->where('user_invoices.year', '=', session('payment_sales_vol_filter_year'))
->get();
$objects = [];
foreach($UserInvoices as $UserInvoice){
if($UserInvoice->shopping_order){
$ShoppingOrder = $UserInvoice->shopping_order;
if($ShoppingOrder->payment_for === 5){ //homeparty
if($ShoppingOrder->homeparty){
foreach($ShoppingOrder->homeparty->homeparty_order_items as $homeparty_item){
if(isset($objects[$homeparty_item->product_id])){
$value = intval($objects[$homeparty_item->product_id]['value'] + $homeparty_item->qty);
$objects[$homeparty_item->product_id]['value'] = $value;
}else{
$objects[$homeparty_item->product_id] = [
'name' => $homeparty_item->product ? $homeparty_item->product->name : "n/a ".$homeparty_item->product_id,
'number' => $homeparty_item->product ? $homeparty_item->product->number : "n/a ".$homeparty_item->product_id,
'value' => $homeparty_item->qty
];
}
}
}
}elseif($ShoppingOrder->payment_for === 8){ //collective_invoice
if($ShoppingOrder->shopping_collect_order){
foreach($ShoppingOrder->shopping_collect_order->shop_items as $shop_item){
if(isset($objects[$shop_item['pid']])){
$value = intval($objects[$shop_item['pid']]['value'] + $shop_item['qty']);
$objects[$shop_item['pid']]['value'] = $value;
}else{
$objects[$shop_item['pid']] = [
'name' => $shop_item['name'],
'number' => $shop_item['number'],
'value' => $shop_item['qty']
];
}
}
}
}else{
if($ShoppingOrder->shopping_order_items){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
if(isset($objects[$shopping_order_item->product_id])){
$value = intval($objects[$shopping_order_item->product_id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product_id]['value'] = $value;
}else{
$objects[$shopping_order_item->product_id] = [
'name' => $shopping_order_item->product ? $shopping_order_item->product->name : "n/a ".$shopping_order_item->product_id,
'number' => $shopping_order_item->product ? $shopping_order_item->product->number : "n/a ".$shopping_order_item->product_id,
'value' => $shopping_order_item->qty
];
}
}
}
}
$hasSOID[] = $ShoppingOrder->id;
}
}
return $objects;
}
public function datatable(){
/*$collect = collect([
['id' => 1, 'name' => 'John', 'number'=>92012, 'value'=>123],
['id' => 2, 'name' => 'Jane', 'number'=>92012, 'value'=>123],
['id' => 3, 'name' => 'James', 'number'=>92012, 'value'=>123],
]);*/
$objects = $this->initKompaktList();
$collection = collect();
if($objects){
foreach($objects as $key => $obj){
$collection->push([
'id' => $key,
'name' => $obj['name'],
'number' => $obj['number'],
'value' => $obj['value'],
]);
}
}
return \DataTables::of($collection)->toJson();
}
/*
//Auswertung nach ShoppingOrder
//nach Datum created_at wann die Bestellung erstellt wurde
//Ist nicht das Datum der Rechnung, da hier teilweise die Sammelrechnungen oder Zahlungen erst in nächsten Monat erfolgen
public function exportFullList($paid = 1){
$date_start = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d H:i:s');
$ShoppingOrders = ShoppingOrder::where('paid', $paid)->where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$txActions = ['prev' => 'keine Zahlung', 'appointed' => 'offen', 'failed' => 'abbruch', 'paid' => 'bezahlt'];
$headers = array('ID', 'Zahlung', 'Datum', 'EMail', 'ProduktID', 'ProduktNummer', 'ProduktName', 'Anzahl', 'Notiz', 'Gesamt');
$objects = [];
$columns = [];
$hasSOID = [];
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
$value = "";
if($shopping_order_item->product){
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
$value = $shopping_order_item->qty;
}
}
$object = [];
if(in_array($ShoppingOrder->id, $hasSOID)){
$object['ID'] = '';
$object['EMail'] = '';
$object['Zahlung'] = '';
$object['Datum'] = '';
}else{
$object['ID'] = $ShoppingOrder->id;
$object['EMail'] = $ShoppingOrder->shopping_user ? $ShoppingOrder->shopping_user->billing_email : 'n/a';
$object['Zahlung'] = isset($txActions[$ShoppingOrder->txaction]) ? $txActions[$ShoppingOrder->txaction] : $ShoppingOrder->txaction;
$object['Datum'] = $ShoppingOrder->created_at->format('d.m.Y');
}
$object['ProduktID'] = $shopping_order_item->product_id;
$object['ProduktNummer'] = $shopping_order_item->product ? $shopping_order_item->product->number : "n/a";
$object['ProduktName'] = $shopping_order_item->product ? $shopping_order_item->product->name : "n/a";
$object['Anzahl'] = $shopping_order_item->qty;
$object['Notiz'] = ($shopping_order_item->comp ? 'Compensation '.$shopping_order_item->comp : '') . ($shopping_order_item->shopping_collect_order_id ? 'Sammelbestellung '.$shopping_order_item->shopping_collect_order_id : '');
$object['Gesamt'] = $value;
$columns[] = $object;
$hasSOID[] = $ShoppingOrder->id;
}
}
if($paid){
$filename = "mivita-absatzmengen-full-paid-".session('payment_sales_vol_filter_month').'_'.session('payment_sales_vol_filter_year')."-export";
}else{
$filename = "mivita-absatzmengen-full-unpaid-".session('payment_sales_vol_filter_month').'_'.session('payment_sales_vol_filter_year')."-export";
}
return Excel::download(new xExport($columns, $headers), $filename.'.xls');
//CSV EXPORT function
$headers = array(
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
);
$header = array('ID', 'Zahlung', 'Datum', 'EMail', 'ProduktID', 'ProduktNummer', 'ProduktName', 'Anzahl', 'Notiz', 'Gesamt');
$callback = function() use($columns, $header) {
$file = fopen('php://output', 'w');
fputcsv($file, $header);
$row = [];
foreach ($columns as $row) {
fputcsv($file, $row);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
*/
/*
//alte Funktion auswerung nach ShoppingOrder
private function testCheckFunction(){
//$date_start = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->format('Y-m-d');
//$date_end = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d');
$date_start = Carbon::parse('01.01.2024')->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.01.2024')->endOfMonth()->format('Y-m-d H:i:s');
dump($date_start);
dump($date_end);
$ShoppingOrders = ShoppingOrder::where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$objects = [];
$counter = 0;
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
if($shopping_order_item->product){
if($shopping_order_item->product->id === 122){
//dump($shopping_order_item->qty);
//$counter += $shopping_order_item->qty;
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
}
}
}
}
}
$ShoppingOrderItems = ShoppingOrderItem::whereProductId(122)->whereBetween('created_at', [$date_start, $date_end])->get();
$counter = 0;
foreach($ShoppingOrderItems as $ShoppingOrderItem){
$counter += $ShoppingOrderItem->qty;
dump($ShoppingOrderItem->id);
}
// dump($objects);
dump($counter);
dd("OKAY");
}*/
/*
// alte Funktion auswerung nach ShoppingOrder
private function initSearch($returnColl = true)
{
$this->setFilterVars();
$date_start = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.'.session('payment_sales_vol_filter_month').'.'.session('payment_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d H:i:s');
$ShoppingOrders = ShoppingOrder::where('paid', 1)->where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$objects = [];
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
if($shopping_order_item->product){
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
}
}
}
}
if($returnColl){
$collection = collect();
foreach($objects as $key => $obj){
$collection->push([
'id' => $key,
'name' => $obj['name'],
'number' => $obj['number'],
'value' => $obj['value'],
]);
}
return $collection;
}
return $objects;
}
*/
}

View file

@ -1,342 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Auth;
use Request;
use App\User;
use Carbon\Carbon;
use App\Exports\xExport;
use App\Services\HTMLHelper;
use App\Models\ShoppingOrder;
use App\Exports\UserTeamExport;
use App\Models\ShoppingOrderItem;
use App\Http\Controllers\Controller;
use Maatwebsite\Excel\Facades\Excel;
use App\Services\BusinessPlan\ExportBot;
use Illuminate\Database\Eloquent\Collection;
class ProductsSalesController extends Controller
{
public function __construct()
{
$this->middleware('admin');
}
public function index()
{
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_years' => HTMLHelper::getYearRange(2022),
];
return view('admin.payment.salesvolume', $data);
}
public function download(){
/*
EXCEL EXPORT function */
if(Request::get('action') === "exportfull_paid"){
return $this->exportFullList(1);
}
if(Request::get('action') === "exportfull_unpaid"){
return $this->exportFullList(0);
}
if(Request::get('action') === "exportfull_paid_invoice"){
return $this->exportFullListInvoice();
}
if(Request::get('action') === "export"){
$objects = $this->initSearch(false);
$columns = [];
$filename = "mivita-absatzmengen-".session('product_sales_vol_filter_month').'_'.session('product_sales_vol_filter_year')."-export";
$headers = array(
'#',
'Produkt',
'Artikelnummer',
'Menge',
);
if($objects){
foreach ($objects as $key => $obj){
$columns[] = array(
'id' => $key,
'name' => $obj['name'],
'number' => $obj['number'],
'value' => $obj['value'],
);
}
}
return Excel::download(new UserTeamExport($columns, $headers), $filename.'.xls');
}
}
private function setFilterVars(){
if(!session('product_sales_vol_filter_month')){
session(['product_sales_vol_filter_month' => intval(date('m'))]);
}
if(!session('product_sales_vol_filter_year')){
session(['product_sales_vol_filter_year' => intval(date('Y'))]);
}
if(Request::get('product_sales_vol_filter_month')){
session(['product_sales_vol_filter_month' => Request::get('product_sales_vol_filter_month')]);
}
if(Request::get('product_sales_vol_filter_year')){
session(['product_sales_vol_filter_year' => Request::get('product_sales_vol_filter_year')]);
}
}
public function exportFullList($paid = 1){
$date_start = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d H:i:s');
$ShoppingOrders = ShoppingOrder::where('paid', $paid)->where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$txActions = ['prev' => 'keine Zahlung', 'appointed' => 'offen', 'failed' => 'abbruch', 'paid' => 'bezahlt'];
$headers = array('ID', 'Zahlung', 'Datum', 'EMail', 'ProduktID', 'ProduktNummer', 'ProduktName', 'Anzahl', 'Notiz', 'Gesamt');
$objects = [];
$columns = [];
$hasSOID = [];
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
$value = "";
if($shopping_order_item->product){
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
$value = $shopping_order_item->qty;
}
}
$object = [];
if(in_array($ShoppingOrder->id, $hasSOID)){
$object['ID'] = '';
$object['EMail'] = '';
$object['Zahlung'] = '';
$object['Datum'] = '';
}else{
$object['ID'] = $ShoppingOrder->id;
$object['EMail'] = $ShoppingOrder->shopping_user ? $ShoppingOrder->shopping_user->billing_email : 'n/a';
$object['Zahlung'] = isset($txActions[$ShoppingOrder->txaction]) ? $txActions[$ShoppingOrder->txaction] : $ShoppingOrder->txaction;
$object['Datum'] = $ShoppingOrder->created_at->format('d.m.Y');
}
$object['ProduktID'] = $shopping_order_item->product_id;
$object['ProduktNummer'] = $shopping_order_item->product ? $shopping_order_item->product->number : "n/a";
$object['ProduktName'] = $shopping_order_item->product ? $shopping_order_item->product->name : "n/a";
$object['Anzahl'] = $shopping_order_item->qty;
$object['Notiz'] = ($shopping_order_item->comp ? 'Compensation '.$shopping_order_item->comp : '') . ($shopping_order_item->shopping_collect_order_id ? 'Sammelbestellung '.$shopping_order_item->shopping_collect_order_id : '');
$object['Gesamt'] = $value;
$columns[] = $object;
$hasSOID[] = $ShoppingOrder->id;
}
}
if($paid){
$filename = "mivita-absatzmengen-full-paid-".session('product_sales_vol_filter_month').'_'.session('product_sales_vol_filter_year')."-export";
}else{
$filename = "mivita-absatzmengen-full-unpaid-".session('product_sales_vol_filter_month').'_'.session('product_sales_vol_filter_year')."-export";
}
return Excel::download(new xExport($columns, $headers), $filename.'.xls');
/* CSV EXPORT function
$headers = array(
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
);
$header = array('ID', 'Zahlung', 'Datum', 'EMail', 'ProduktID', 'ProduktNummer', 'ProduktName', 'Anzahl', 'Notiz', 'Gesamt');
$callback = function() use($columns, $header) {
$file = fopen('php://output', 'w');
fputcsv($file, $header);
$row = [];
foreach ($columns as $row) {
fputcsv($file, $row);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
*/
}
private function initSearch($returnColl = true)
{
$this->setFilterVars();
$date_start = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d H:i:s');
$ShoppingOrders = ShoppingOrder::where('paid', 1)->where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$objects = [];
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
if($shopping_order_item->product){
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
}
}
}
}
if($returnColl){
$collection = collect();
foreach($objects as $key => $obj){
$collection->push([
'id' => $key,
'name' => $obj['name'],
'number' => $obj['number'],
'value' => $obj['value'],
]);
}
return $collection;
}
return $objects;
}
public function datatable(){
$collection = $this->initSearch(true);
$collect = collect([
['id' => 1, 'name' => 'John', 'number'=>92012, 'value'=>123],
['id' => 2, 'name' => 'Jane', 'number'=>92012, 'value'=>123],
['id' => 3, 'name' => 'James', 'number'=>92012, 'value'=>123],
]);
return \DataTables::of($collection)->toJson();
}
/*private function export_vp(){
$query = User::with('account')->select('users.*')->where('users.deleted_at', '=', null)->where('users.admin', "<", 4)->get();
$fileName = "GS-VP-export-".date("d-m-Y").".csv";
$headers = array(
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
);
$columns = array('ID', 'Email', 'Firma', 'Anrede', 'Vorname', 'Nachname', 'Mitglied', 'Bis');
$callback = function() use($query, $columns) {
$file = fopen('php://output', 'w');
fputcsv($file, $columns);
$row = [];
foreach ($query as $val) {
$row['ID'] = $val->id;
$row['Email'] = $val->email;
$row['Firma'] = $val->account->company;
$row['Anrede'] = $val->account->salutation == 'mr' ? 'Herr' : 'Frau' ;
$row['Vorname'] = $val->account->first_name;
$row['Nachname'] = $val->account->last_name;
$row['Mitglied'] = $val->payment_account ? ($val->isActiveAccount() ? 'JA' : 'Abgelaufen') : "Nein";
$row['Bis'] = $val->payment_account ? $val->getPaymentAccountDateFormat(false) : "-";
fputcsv($file, $row);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
//dd("ok");
}*/
/*private function testCheckFunction(){
//$date_start = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->format('Y-m-d');
//$date_end = Carbon::parse('01.'.session('product_sales_vol_filter_month').'.'.session('product_sales_vol_filter_year'))->endOfMonth()->format('Y-m-d');
$date_start = Carbon::parse('01.01.2024')->format('Y-m-d H:i:s');
$date_end = Carbon::parse('01.01.2024')->endOfMonth()->format('Y-m-d H:i:s');
dump($date_start);
dump($date_end);
$ShoppingOrders = ShoppingOrder::where('mode', 'live')->whereBetween('created_at', [$date_start, $date_end])->get();
$objects = [];
$counter = 0;
foreach($ShoppingOrders as $ShoppingOrder){
foreach($ShoppingOrder->shopping_order_items as $shopping_order_item){
if($shopping_order_item->product){
if($shopping_order_item->product->id === 122){
//dump($shopping_order_item->qty);
//$counter += $shopping_order_item->qty;
if(isset($objects[$shopping_order_item->product->id])){
$value = intval($objects[$shopping_order_item->product->id]['value'] + $shopping_order_item->qty);
$objects[$shopping_order_item->product->id]['value'] = $value;
}else{
$objects[$shopping_order_item->product->id] = [
'name' => $shopping_order_item->product->name,
'number' => $shopping_order_item->product->number,
'value' => $shopping_order_item->qty
];
}
}
}
}
}
$ShoppingOrderItems = ShoppingOrderItem::whereProductId(122)->whereBetween('created_at', [$date_start, $date_end])->get();
$counter = 0;
foreach($ShoppingOrderItems as $ShoppingOrderItem){
$counter += $ShoppingOrderItem->qty;
dump($ShoppingOrderItem->id);
}
// dump($objects);
dump($counter);
dd("OKAY");
}*/
}

View file

@ -1,708 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\ShoppingUser;
use App\Models\ShoppingUserMemberLog;
use App\Models\UserCleanUpLog;
use App\User;
use Illuminate\Support\Facades\Auth;
class AdminUserCleanupController extends Controller
{
public function __construct()
{
$this->middleware('superadmin');
}
/**
* Übersicht deaktivierter und gelöschter User
*/
public function index()
{
return view('admin.user.cleanup.index');
}
/**
* Protokoll der User-Cleanup-Logs (Downline-Übertragungen)
*/
public function logs()
{
return view('admin.user.cleanup.logs');
}
/**
* Protokoll der Shopping-User-Member-Logs (Kunden-Übertragungen)
*/
public function shoppingLogs()
{
return view('admin.user.cleanup.shopping_logs');
}
/**
* DataTable für deaktivierte/gelöschte User
*/
public function getInactiveUsers()
{
// Deaktivierte User (active=false) ODER gelöschte User (mit pre_deleted_at)
$query = User::withTrashed()
->where(function ($q) {
$q->where('active', false)
->orWhere('pre_deleted_at', '!=', null);
})
->with('account')
->select('users.*')
->where('users.admin', '<', 5);
return \DataTables::eloquent($query)
->addColumn('user_id', function (User $user) {
return $user->id;
})
->addColumn('first_name', function (User $user) {
return $user->account ? $user->account->first_name : '';
})
->addColumn('last_name', function (User $user) {
return $user->account ? $user->account->last_name : '';
})
->addColumn('email', function (User $user) {
if ($user->pre_deleted_at) {
return '<span class="badge badge-pill badge-danger">'.$user->email.'</span>';
}
return $user->email;
})
->addColumn('m_account', function (User $user) {
return $user->account ? $user->account->m_account : '';
})
->addColumn('status', function (User $user) {
if ($user->pre_deleted_at) {
return '<span class="badge badge-danger">Gelöscht</span>';
}
if (! $user->active) {
return '<span class="badge badge-warning">Deaktiviert</span>';
}
return '<span class="badge badge-success">Aktiv</span>';
})
->addColumn('deleted_at', function (User $user) {
if ($user->pre_deleted_at) {
return \Carbon\Carbon::parse($user->pre_deleted_at)->format('d.m.Y H:i');
}
return '-';
})
->addColumn('payment_account', function (User $user) {
return $user->getPaymentAccountDateFormat();
})
->addColumn('m_sponsor', function (User $user) {
if ($user->m_sponsor) {
$sponsor = User::find($user->m_sponsor);
return $sponsor ? $sponsor->email : 'ID: '.$user->m_sponsor;
}
return '-';
})
->addColumn('pre_sponsor', function (User $user) {
if ($user->pre_sponsor) {
$sponsor = User::withTrashed()->find($user->pre_sponsor);
return $sponsor ? $sponsor->email : 'ID: '.$user->pre_sponsor;
}
return '-';
})
->addColumn('childs_count', function (User $user) {
$count = User::where('m_sponsor', $user->id)->count();
return $count > 0 ? '<span class="badge badge-info">'.$count.'</span>' : '0';
})
->addColumn('shopping_users_count', function (User $user) {
$count = ShoppingUser::where('member_id', $user->id)->count();
return $count > 0 ? '<span class="badge badge-info">'.$count.'</span>' : '0';
})
->addColumn('action', function (User $user) {
$html = '';
if ($user->pre_deleted_at) {
$html .= '<a href="'.route('admin_lead_edit', [$user->id]).'" class="btn btn-sm btn-info" title="Details"><i class="fa fa-eye"></i></a> ';
} else {
$html .= '<a href="'.route('admin_lead_edit', [$user->id]).'" class="btn btn-sm btn-primary" title="Bearbeiten"><i class="fa fa-edit"></i></a> ';
}
// Historie-Button
$html .= ' <button class="btn btn-sm btn-secondary btn-user-history" data-id="'.$user->id.'" data-email="'.$user->email.'" title="Historie & Details"><i class="fa fa-history"></i></button>';
// Restore-Button für gelöschte User
if ($user->pre_deleted_at) {
$html .= ' <button class="btn btn-sm btn-success" data-toggle="modal" data-target="#modal-restore-user" data-id="'.$user->id.'" data-email="'.str_replace('delete-', '', $user->email).'" title="Wiederherstellen"><i class="fa fa-undo"></i></button>';
}
return $html;
})
->orderColumn('user_id', 'id $1')
->orderColumn('email', 'email $1')
->orderColumn('status', 'active $1')
->rawColumns(['email', 'status', 'childs_count', 'shopping_users_count', 'action'])
->make(true);
}
/**
* DataTable für UserCleanUpLogs (Downline-Übertragungen)
*/
public function getCleanupLogs()
{
$query = UserCleanUpLog::with(['inactive_sponsor.account', 'child_user.account', 'new_sponsor.account'])
->select('user_clean_up_logs.*');
return \DataTables::eloquent($query)
->addColumn('id', function (UserCleanUpLog $log) {
return $log->id;
})
->addColumn('inactive_sponsor', function (UserCleanUpLog $log) {
if ($log->inactive_sponsor && $log->inactive_sponsor->account) {
$name = trim($log->inactive_sponsor->account->first_name.' '.$log->inactive_sponsor->account->last_name);
return ($name ?: 'N/A').'<br><small>'.$log->inactive_sponsor->email.'</small>';
}
return 'ID: '.$log->inactive_sponsor_id;
})
->addColumn('child_user', function (UserCleanUpLog $log) {
if ($log->child_user && $log->child_user->account) {
$name = trim($log->child_user->account->first_name.' '.$log->child_user->account->last_name);
return ($name ?: 'N/A').'<br><small>'.$log->child_user->email.'</small>';
}
return 'ID: '.$log->child_user_id;
})
->addColumn('new_sponsor', function (UserCleanUpLog $log) {
$html = '';
// Original-Sponsor aus dem Log
if ($log->new_sponsor && $log->new_sponsor->account) {
$name = trim($log->new_sponsor->account->first_name.' '.$log->new_sponsor->account->last_name);
$html .= '<span class="text-muted"><small>Damals:</small></span><br>';
$html .= ($name ?: 'N/A').'<br><small>'.$log->new_sponsor->email.'</small>';
} else {
$html .= 'ID: '.$log->new_sponsor_id;
}
// Prüfe aktuellen Sponsor des child_user
if ($log->child_user) {
$currentSponsorId = $log->child_user->m_sponsor;
// Wenn aktueller Sponsor ANDERS ist als im Log
if ($currentSponsorId && $currentSponsorId != $log->new_sponsor_id) {
$currentSponsor = User::with('account')->find($currentSponsorId);
if ($currentSponsor) {
$html .= '<hr class="my-1">';
$html .= '<span class="badge badge-warning">Geändert!</span><br>';
$html .= '<span class="text-success"><small>Aktuell:</small></span><br>';
if ($currentSponsor->account) {
$currentName = trim($currentSponsor->account->first_name.' '.$currentSponsor->account->last_name);
$html .= '<strong>'.($currentName ?: 'N/A').'</strong><br>';
}
$html .= '<small>'.$currentSponsor->email.'</small>';
}
}
}
return $html;
})
->addColumn('created_at', function (UserCleanUpLog $log) {
return \Carbon\Carbon::parse($log->created_at)->format('d.m.Y H:i');
})
->orderColumn('id', 'id $1')
->rawColumns(['inactive_sponsor', 'child_user', 'new_sponsor'])
->make(true);
}
/**
* DataTable für ShoppingUserMemberLogs (Kunden-Übertragungen)
*/
public function getShoppingLogs()
{
$query = ShoppingUserMemberLog::with(['pre_member.account', 'shopping_user', 'new_member.account'])
->select('shopping_user_member_logs.*');
return \DataTables::eloquent($query)
->addColumn('id', function (ShoppingUserMemberLog $log) {
return $log->id;
})
->addColumn('pre_member', function (ShoppingUserMemberLog $log) {
if ($log->pre_member && $log->pre_member->account) {
$name = trim($log->pre_member->account->first_name.' '.$log->pre_member->account->last_name);
return ($name ?: 'N/A').'<br><small>'.$log->pre_member->email.'</small>';
}
return 'ID: '.$log->pre_member_id;
})
->addColumn('shopping_user', function (ShoppingUserMemberLog $log) {
if ($log->shopping_user) {
$name = trim($log->shopping_user->billing_firstname.' '.$log->shopping_user->billing_lastname);
return ($name ?: 'N/A').'<br><small>'.$log->shopping_user->billing_email.'</small>';
}
return 'ID: '.$log->shopping_user_id;
})
->addColumn('new_member', function (ShoppingUserMemberLog $log) {
$html = '';
// Original-Berater aus dem Log
if ($log->new_member && $log->new_member->account) {
$name = trim($log->new_member->account->first_name.' '.$log->new_member->account->last_name);
$html .= '<span class="text-muted"><small>Damals:</small></span><br>';
$html .= ($name ?: 'N/A').'<br><small>'.$log->new_member->email.'</small>';
} else {
$html .= 'ID: '.$log->new_member_id;
}
// Prüfe aktuellen Berater des Kunden
if ($log->shopping_user) {
$currentMemberId = $log->shopping_user->member_id;
// Wenn aktueller Berater ANDERS ist als im Log
if ($currentMemberId && $currentMemberId != $log->new_member_id) {
$currentMember = User::with('account')->find($currentMemberId);
if ($currentMember) {
$html .= '<hr class="my-1">';
$html .= '<span class="badge badge-warning">Geändert!</span><br>';
$html .= '<span class="text-success"><small>Aktuell:</small></span><br>';
if ($currentMember->account) {
$currentName = trim($currentMember->account->first_name.' '.$currentMember->account->last_name);
$html .= '<strong>'.($currentName ?: 'N/A').'</strong><br>';
}
$html .= '<small>'.$currentMember->email.'</small>';
}
}
}
return $html;
})
->addColumn('created_at', function (ShoppingUserMemberLog $log) {
return \Carbon\Carbon::parse($log->created_at)->format('d.m.Y H:i');
})
->orderColumn('id', 'id $1')
->rawColumns(['pre_member', 'shopping_user', 'new_member'])
->make(true);
}
/**
* User über Artisan Command wiederherstellen
*/
public function restore(\Illuminate\Http\Request $request)
{
$userId = $request->input('user_id');
if (! $userId) {
return response()->json([
'success' => false,
'message' => 'Keine User-ID angegeben',
], 400);
}
try {
// Führe Artisan Command aus
\Artisan::call('user:restore', ['user_id' => $userId]);
$output = \Artisan::output();
// Prüfe ob Command erfolgreich war (Exit Code 0)
$exitCode = \Artisan::call('user:restore', ['user_id' => $userId]);
\Log::channel('cleanup')->info('AdminUserCleanupController restore via web: user_id='.$userId.' | exitCode='.$exitCode);
if ($exitCode === 0) {
return response()->json([
'success' => true,
'message' => 'User wurde erfolgreich wiederhergestellt',
'output' => $output,
]);
} else {
return response()->json([
'success' => false,
'message' => 'Fehler beim Wiederherstellen (Exit Code: '.$exitCode.')',
'output' => $output,
], 500);
}
} catch (\Exception $e) {
\Log::channel('cleanup')->error('AdminUserCleanupController restore failed: '.$e->getMessage());
return response()->json([
'success' => false,
'message' => 'Exception: '.$e->getMessage(),
], 500);
}
}
/**
* Suche nach aktiven Sponsoren für Select2
*/
public function searchSponsors(\Illuminate\Http\Request $request)
{
$search = $request->input('q');
$userId = $request->input('exclude_user_id'); // User selbst ausschließen
$loadAll = $request->input('load_all', false); // Alle Sponsoren laden
$query = User::where('active', true)
->where('admin', '<', 5)
->where('blocked', false)
->where('payment_account', '>=', now())
->with('account')
->orderBy('email', 'asc');
if ($userId) {
$query->where('id', '!=', $userId);
}
// Nur filtern wenn Suche vorhanden und nicht load_all
if ($search && ! $loadAll) {
$query->where(function ($q) use ($search) {
$q->where('email', 'like', '%'.$search.'%')
->orWhereHas('account', function ($q2) use ($search) {
$q2->where('first_name', 'like', '%'.$search.'%')
->orWhere('last_name', 'like', '%'.$search.'%')
->orWhere('m_account', 'like', '%'.$search.'%');
});
});
}
// Limit nur wenn nicht alle geladen werden sollen
if (! $loadAll) {
$query->limit(20);
}
$users = $query->get()->map(function ($user) {
$name = '';
if ($user->account) {
$name = trim($user->account->first_name.' '.$user->account->last_name);
if ($name) {
$name .= ' | ';
}
}
return [
'id' => $user->id,
'text' => $name.$user->email.($user->account && $user->account->m_account ? ' #'.$user->account->m_account : ''),
];
});
return response()->json(['results' => $users]);
}
/**
* Sponsor manuell neu zuweisen
*/
public function reassignSponsor(\Illuminate\Http\Request $request)
{
$userId = $request->input('user_id');
$newSponsorId = $request->input('new_sponsor_id');
// Boolean-Werte korrekt konvertieren (auch wenn sie als String ankommen)
$transferDownline = filter_var($request->input('transfer_downline', false), FILTER_VALIDATE_BOOLEAN);
$transferCustomers = filter_var($request->input('transfer_customers', false), FILTER_VALIDATE_BOOLEAN);
if (! $userId || ! $newSponsorId) {
return response()->json([
'success' => false,
'message' => 'User-ID und neuer Sponsor sind erforderlich',
], 400);
}
$user = User::withTrashed()->find($userId);
$newSponsor = User::find($newSponsorId);
if (! $user) {
return response()->json([
'success' => false,
'message' => 'User nicht gefunden',
], 404);
}
if (! $newSponsor || ! $newSponsor->active) {
return response()->json([
'success' => false,
'message' => 'Neuer Sponsor nicht gefunden oder nicht aktiv',
], 404);
}
\DB::beginTransaction();
try {
$oldSponsorId = $user->m_sponsor;
$logs = [];
// 1. Downline neu zuweisen (aus Logs - bereits übertragene)
$childrenTransferred = 0;
if ($transferDownline) {
// Hole die Kinder aus den vorherigen Cleanup-Logs (die bereits VON diesem User weg übertragen wurden)
$cleanupLogs = UserCleanUpLog::where('inactive_sponsor_id', $userId)->get();
\Log::channel('cleanup')->info('Reassigning downline from logs: found '.$cleanupLogs->count().' log entries for user_id='.$userId);
foreach ($cleanupLogs as $oldLog) {
$child = User::find($oldLog->child_user_id);
if (! $child) {
\Log::channel('cleanup')->warning('Child user not found: '.$oldLog->child_user_id);
continue;
}
// Neuen Log erstellen für die Neu-Zuweisung
UserCleanUpLog::create([
'inactive_sponsor_id' => $child->m_sponsor, // Aktueller Sponsor (wohin es vorher übertragen wurde)
'child_user_id' => $child->id,
'new_sponsor_id' => $newSponsorId, // Neuer Sponsor
]);
// Sponsor ändern
$child->m_sponsor = $newSponsorId;
$child->save();
$childrenTransferred++;
$logs[] = 'Downline: '.$child->email.' → Neuer Sponsor: '.$newSponsor->email;
}
\Log::channel('cleanup')->info('Children reassigned: '.$childrenTransferred);
}
// 2. Shopping-Kunden neu zuweisen (aus Logs - bereits übertragene)
$customersTransferred = 0;
if ($transferCustomers) {
// Hole die Kunden aus den vorherigen Shopping-Logs (die bereits VON diesem User weg übertragen wurden)
$shoppingLogs = ShoppingUserMemberLog::where('pre_member_id', $userId)->get();
\Log::channel('cleanup')->info('Reassigning customers from logs: found '.$shoppingLogs->count().' log entries for user_id='.$userId);
foreach ($shoppingLogs as $oldLog) {
$customer = ShoppingUser::find($oldLog->shopping_user_id);
if (! $customer) {
\Log::channel('cleanup')->warning('Shopping user not found: '.$oldLog->shopping_user_id);
continue;
}
// Neuen Log erstellen für die Neu-Zuweisung
ShoppingUserMemberLog::create([
'pre_member_id' => $customer->member_id, // Aktueller Berater (wohin es vorher übertragen wurde)
'shopping_user_id' => $customer->id,
'new_member_id' => $newSponsorId, // Neuer Berater
]);
// Member ändern
$customer->member_id = $newSponsorId;
$customer->save();
$customersTransferred++;
$logs[] = 'Kunde: '.$customer->billing_email.' → Neuer Berater: '.$newSponsor->email;
}
\Log::channel('cleanup')->info('Customers reassigned: '.$customersTransferred);
}
// 3. User selbst dem neuen Sponsor zuweisen
$user->m_sponsor = $newSponsorId;
$user->save();
\DB::commit();
// Cleanup-Log
\Log::channel('cleanup')->info('Manual reassign sponsor: user_id='.$userId.' | old_sponsor='.$oldSponsorId.' | new_sponsor='.$newSponsorId.' | transfer_downline='.(int) $transferDownline.' | transfer_customers='.(int) $transferCustomers.' | by_admin='.Auth::id());
return response()->json([
'success' => true,
'message' => 'Sponsor erfolgreich neu zugewiesen',
'logs' => $logs,
'transferred' => [
'downline' => $childrenTransferred,
'customers' => $customersTransferred,
],
]);
} catch (\Exception $e) {
\DB::rollBack();
\Log::channel('cleanup')->error('Manual reassign sponsor failed: user_id='.$userId.' | error='.$e->getMessage());
return response()->json([
'success' => false,
'message' => 'Fehler beim Neu-Zuweisen: '.$e->getMessage(),
], 500);
}
}
/**
* Lade User-Historie: Downline-Position und Shopping-Kunden
*/
public function getUserHistory($userId)
{
$user = User::withTrashed()->with('account')->find($userId);
if (! $user) {
return response()->json([
'success' => false,
'message' => 'User nicht gefunden',
], 404);
}
// Aktueller/Vorheriger Sponsor
$sponsor = null;
$preSponsor = null;
if ($user->m_sponsor) {
$sponsor = User::withTrashed()->with('account')->find($user->m_sponsor);
}
if ($user->pre_sponsor) {
$preSponsor = User::withTrashed()->with('account')->find($user->pre_sponsor);
}
// Direkte Downline (Kinder) - nur die, die aktuell m_sponsor haben
// pre_sponsor sind bereits deaktiviert und würden doppelt gezählt
$children = User::withTrashed()
->with('account')
->where('m_sponsor', $userId)
->get()
->map(function ($child) use ($userId) {
return [
'id' => $child->id,
'name' => $child->account ? trim($child->account->first_name.' '.$child->account->last_name) : 'N/A',
'email' => $child->email,
'active' => $child->active,
'deleted' => $child->pre_deleted_at ? true : false,
'is_pre_sponsor' => $child->pre_sponsor == $userId,
];
});
// Shopping-Kunden
$shoppingUsers = ShoppingUser::where('member_id', $userId)
->get()
->map(function ($customer) {
return [
'id' => $customer->id,
'name' => trim($customer->billing_firstname.' '.$customer->billing_lastname),
'email' => $customer->billing_email,
'city' => $customer->billing_city,
'created_at' => \Carbon\Carbon::parse($customer->created_at)->format('d.m.Y'),
];
});
// Downline-Übertragungen (wo dieser User betroffen war)
$cleanupLogs = UserCleanUpLog::with(['inactive_sponsor.account', 'child_user.account', 'new_sponsor.account'])
->where(function ($query) use ($userId) {
$query->where('inactive_sponsor_id', $userId)
->orWhere('child_user_id', $userId);
})
->orderBy('created_at', 'desc')
->get()
->map(function ($log) use ($userId) {
$data = [
'type' => $log->inactive_sponsor_id == $userId ? 'as_inactive' : 'as_child',
'child_user' => $log->child_user ? [
'id' => $log->child_user->id,
'name' => $log->child_user->account ? trim($log->child_user->account->first_name.' '.$log->child_user->account->last_name) : 'N/A',
'email' => $log->child_user->email,
'active' => $log->child_user->active,
'deleted' => $log->child_user->pre_deleted_at ? true : false,
] : null,
'inactive_sponsor' => $log->inactive_sponsor ? [
'id' => $log->inactive_sponsor->id,
'name' => $log->inactive_sponsor->account ? trim($log->inactive_sponsor->account->first_name.' '.$log->inactive_sponsor->account->last_name) : 'N/A',
'email' => $log->inactive_sponsor->email,
] : null,
'new_sponsor' => $log->new_sponsor ? [
'id' => $log->new_sponsor->id,
'name' => $log->new_sponsor->account ? trim($log->new_sponsor->account->first_name.' '.$log->new_sponsor->account->last_name) : 'N/A',
'email' => $log->new_sponsor->email,
] : null,
'created_at' => \Carbon\Carbon::parse($log->created_at)->format('d.m.Y H:i'),
];
// Prüfe aktuellen Sponsor des child_user (falls geändert)
if ($log->child_user && $log->child_user->m_sponsor && $log->child_user->m_sponsor != $log->new_sponsor_id) {
$currentSponsor = User::with('account')->find($log->child_user->m_sponsor);
if ($currentSponsor) {
$data['current_sponsor'] = [
'id' => $currentSponsor->id,
'name' => $currentSponsor->account ? trim($currentSponsor->account->first_name.' '.$currentSponsor->account->last_name) : 'N/A',
'email' => $currentSponsor->email,
];
}
}
return $data;
});
// Kunden-Übertragungen
$shoppingLogs = ShoppingUserMemberLog::with(['shopping_user', 'new_member.account'])
->where('pre_member_id', $userId)
->orderBy('created_at', 'desc')
->get()
->map(function ($log) {
$data = [
'customer_name' => $log->shopping_user ? trim($log->shopping_user->billing_firstname.' '.$log->shopping_user->billing_lastname) : 'N/A',
'customer_email' => $log->shopping_user ? $log->shopping_user->billing_email : 'N/A',
'new_member' => $log->new_member ? [
'id' => $log->new_member->id,
'name' => $log->new_member->account ? trim($log->new_member->account->first_name.' '.$log->new_member->account->last_name) : 'N/A',
'email' => $log->new_member->email,
] : null,
'created_at' => \Carbon\Carbon::parse($log->created_at)->format('d.m.Y H:i'),
];
// Prüfe aktuellen Berater des Kunden (falls geändert)
if ($log->shopping_user && $log->shopping_user->member_id && $log->shopping_user->member_id != $log->new_member_id) {
$currentMember = User::with('account')->find($log->shopping_user->member_id);
if ($currentMember) {
$data['current_member'] = [
'id' => $currentMember->id,
'name' => $currentMember->account ? trim($currentMember->account->first_name.' '.$currentMember->account->last_name) : 'N/A',
'email' => $currentMember->email,
];
}
}
return $data;
});
return response()->json([
'success' => true,
'user' => [
'id' => $user->id,
'name' => $user->account ? trim($user->account->first_name.' '.$user->account->last_name) : 'N/A',
'email' => $user->email,
'm_account' => $user->account ? $user->account->m_account : 'N/A',
'active' => $user->active,
'deleted' => $user->pre_deleted_at ? true : false,
'deleted_at' => $user->pre_deleted_at ? \Carbon\Carbon::parse($user->pre_deleted_at)->format('d.m.Y H:i') : null,
],
'sponsor' => $sponsor ? [
'id' => $sponsor->id,
'name' => $sponsor->account ? trim($sponsor->account->first_name.' '.$sponsor->account->last_name) : 'N/A',
'email' => $sponsor->email,
'active' => $sponsor->active,
] : null,
'pre_sponsor' => $preSponsor ? [
'id' => $preSponsor->id,
'name' => $preSponsor->account ? trim($preSponsor->account->first_name.' '.$preSponsor->account->last_name) : 'N/A',
'email' => $preSponsor->email,
'active' => $preSponsor->active,
] : null,
'children' => $children,
'shopping_users' => $shoppingUsers,
'cleanup_logs' => $cleanupLogs,
'shopping_logs' => $shoppingLogs,
]);
}
}

272
app/Http/Controllers/AdminUserController.php Normal file → Executable file
View file

@ -4,19 +4,18 @@ namespace App\Http\Controllers;
use Auth;
use Carbon;
use Request;
use App\User;
use Validator;
use App\Services\SysLog;
use App\Services\UserUtil;
use App\Models\UserAccount;
use App\Services\HTMLHelper;
use App\Models\PaymentMethod;
use App\Models\UserAccount;
use App\Repositories\UserRepository;
use App\Services\HTMLHelper;
use App\Services\SysLog;
use App\User;
use Auth;
use Illuminate\Support\Facades\Mail;
use Util;
use Request;
use Validator;
class AdminUserController extends Controller
{
@ -26,6 +25,7 @@ class AdminUserController extends Controller
{
$this->middleware('superadmin');
$this->userRepo = $userRepo;
}
/**
@ -43,7 +43,7 @@ class AdminUserController extends Controller
public function edit($user_id)
{
$user = User::findOrFail($user_id);
if (!$user->account) {
if(!$user->account){
$user->account = new UserAccount();
}
@ -51,161 +51,119 @@ class AdminUserController extends Controller
'user' => $user,
];
return view('admin.user.edit', $data);
}
/**
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function store()
public function store(Request $request)
{
$data = Request::all();
$user = User::withTrashed()->findOrFail($data['id']);
/* if(isset($data['user-delete'])){
$user = User::findOrFail($data['id']);
if(isset($data['user-delete'])){
if(isset($data['realy_delete_user'])){
return redirect(route('admin_user_delete', [$user->id]));
}
}*/
if (isset($data['save-admin'])) {
}
if(isset($data['save-admin'])){
$user->admin = $data['admin'];
SysLog::action('save-admin', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user admin value: ' . HTMLHelper::getLabel($user->admin))
->setMessage('Set user admin value: '.HTMLHelper::getRoleLabel($user->admin))
->save();
}
if (isset($data['save-confirmed'])) {
if(isset($data['save-confirmed'])){
$data['confirmed'] = isset($data['confirmed']) ? true : false;
$user->confirmed = $data['confirmed'];
if ($data['confirmed']) {
if (!isset($data['confirmation_date']) || $data['confirmation_date'] == "") {
if($data['confirmed']){
if(!isset($data['confirmation_date']) || $data['confirmation_date'] == ""){
$user->confirmation_date = now();
} else {
}else{
$user->confirmation_date = \Carbon::parse(str_replace("- ", "", $data['confirmation_date']));
}
} else {
}else{
$user->confirmation_date = null;
}
SysLog::action('save-confirmed', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user confirmed value: ' . $user->confirmed . " to date: " . $data['confirmation_date'])
->setMessage('Set user confirmed value: '.$user->confirmed." to date: ".$data['confirmation_date'])
->save();
}
if (isset($data['save-active'])) {
if(isset($data['save-active'])){
$data['active'] = isset($data['active']) ? true : false;
$user->active = $data['active'];
if ($data['active'] === true && $user->wizard < 20) {
if($data['active'] === true && $user->wizard < 20){
$user->wizard = 20;
}
if ($data['active']) {
if (!isset($data['active_date']) || $data['active_date'] == "") {
if($data['active']){
if(!isset($data['active_date']) || $data['active_date'] == ""){
$user->active_date = now();
} else {
}else{
$user->active_date = \Carbon::parse(str_replace("- ", "", $data['active_date']));
}
} else {
}else{
$user->active_date = null;
}
SysLog::action('save-active', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user active value: ' . $user->active . " to date: " . $data['active_date'])
->setMessage('Set user active value: '.$user->active." to date: ".$data['active_date'])
->save();
}
if (isset($data['save-restore'])) {
$restore_account = isset($data['restore_account']) ? true : false;
$restore_childs = isset($data['restore_childs']) ? true : false;
$restore_send_email = isset($data['restore_send_email']) ? true : false;
if (isset($data['payment_account']) || $data['payment_account'] != "") {
$error = UserUtil::checkEmailExists($user);
if ($error) {
\Session()->flash('alert-error', $error);
return redirect()->back()->withInput()->withErrors(['error' => $error]);
}
$payment_account = \Carbon::parse(str_replace("- ", "", $data['payment_account']));
if ($restore_account) {
UserUtil::restoreUser($user, $payment_account);
if ($restore_childs) {
UserUtil::resetChildsToSponsor($user->id);
}
if ($restore_send_email) {
$user = User::with('account')->findOrFail($data['id']);
Mail::to($user->email)->send(new \App\Mail\UserRestoreEmail($user));
}
}
}
SysLog::action('save-restore', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user restore_account value: ' . $user->restore_account . " and restore_childs value: " . $user->restore_childs)
->save();
}
if (isset($data['save-account'])) {
if(isset($data['save-account'])){
$old = $user->getPaymentAccountDateFormat(true);
if (!isset($data['payment_account']) || $data['payment_account'] == "") {
if(!isset($data['payment_account']) || $data['payment_account'] == ""){
$user->payment_account = null;
} else {
}else{
$user->wizard = 100;
$payment_account = \Carbon::parse(str_replace("- ", "", $data['payment_account']));
$user->payment_account = $payment_account;
if ($payment_account > Carbon::now()) {
if ($user->active === 0) {
$user->active = true;
UserUtil::reactiveUserResetChilds($user->id, 'on save-account AdminUserController');
}
} else {
if ($user->active === 1) {
$user->active = false;
UserUtil::deactiveUserNewSponsorChilds($user->id, 'on save-account AdminUserController');
}
}
$user->payment_account = \Carbon::parse(str_replace("- ", "", $data['payment_account']));
}
//th.schifferegger@gmail.com
SysLog::action('save-account', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user payment_account from date: ' . $old . " to date: " . $data['payment_account'])
->setMessage('Set user payment_account from date: '.$old." to date: ".$data['payment_account'])
->save();
}
if (isset($data['save-shop'])) {
if(isset($data['save-shop'])){
$old = $user->getPaymentShopDateFormat(true);
if (!isset($data['payment_shop']) || $data['payment_shop'] == "") {
if(!isset($data['payment_shop']) || $data['payment_shop'] == ""){
$user->payment_shop = null;
} else {
}else{
$user->wizard = 100;
$user->payment_shop = \Carbon::parse(str_replace("- ", "", $data['payment_shop']));
}
SysLog::action('save-shop', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user payment_shop from date: ' . $old . " to date: " . $data['payment_shop'])
->setMessage('Set user payment_shop from date: '.$old." to date: ".$data['payment_shop'])
->save();
}
if (isset($data['save-test_mode'])) {
if(isset($data['save-test_mode'])){
$user->test_mode = isset($data['test_mode']) ? true : false;
SysLog::action('save-test_mode', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user test_mode value: ' . $user->test_mode)
->setMessage('Set user test_mode value: '.$user->test_mode)
->save();
}
if (isset($data['save-payment_methods'])) {
if(isset($data['save-payment_methods'])){
$user->payment_methods = isset($data['payment_methods']) ? array_map('intval', $data['payment_methods']) : null;
SysLog::action('save-payment_methods', 'admin_user', 3)
->setUserId(Auth::user()->id)
->setModel($user->id, User::class)
->setMessage('Set user payment_methods value: ' . $user->getPaymentMethodsShort())
->setMessage('Set user payment_methods value: '.$user->getPaymentMethodsShort())
->save();
}
@ -215,143 +173,23 @@ class AdminUserController extends Controller
return redirect('/admin/users');
}
public function deleteUser()
public function deleteUser($user_id)
{
$data = Request::all();
$user = User::withTrashed()->findOrFail($data['id']);
if (isset($data['realy_delete_user'])) {
$this->userRepo->deleteUser($user);
\Session()->flash('alert-success', __('msg.contact_delete'));
}
if (isset($data['realy_delete_user_complete'])) {
// $this->userRepo->deleteUserComplete($user);
$this->userRepo->deleteUser($user, true);
\Session()->flash('alert-success', __('msg.contact_delete'));
}
$user = User::findOrFail($user_id);
$this->userRepo->deleteUser($user);
\Session()->flash('alert-success', "Kontakt gelöscht");
return redirect('/admin/users');
}
public function userLoginAs($userId)
{
if (Auth::user()->isSuperAdmin()) {
public function userLoginAs($userId){
if(Auth::user()->isSuperAdmin()){
$user = User::find($userId);
Auth::login($user);
return redirect('/home');
}
}
public function getUsers()
{
$query = User::withTrashed()
->where(function ($q) {
$q->where('pre_deleted_at', '!=', null)
->orWhere(function ($query) {
$query->whereNull('deleted_at')
->whereNull('pre_deleted_at');
});
})
->with('account')
->select('users.*')
->where('users.admin', "<", 5);
return \DataTables::eloquent($query)
->addColumn('first_name', function (User $user) {
return $user->account ? $user->account->first_name : '';
})
->addColumn('email', function (User $user) {
if ($user->pre_deleted_at) {
return '<span class="badge badge-pill badge-danger">' . $user->email . '</span>';
}
return $user->email;
})
->addColumn('last_name', function (User $user) {
return $user->account ? $user->account->last_name : '';
})
->addColumn('id', function (User $user) {
return '<a href="' . route('admin_lead_edit', [$user->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('admin', function (User $user) {
return '<a href="#" data-toggle="modal" data-target="#modals-admin" data-id="' . $user->id . '" data-email="' . $user->email . '" data-admin="' . $user->admin . '">' . HTMLHelper::getRoleLabel($user->admin) . '</a>';
})
->addColumn('confirmed', function (User $user) {
$date = $user->getConfirmationDateFormat();
$link = '<a href="#" data-toggle="modal" data-target="#modals-confirmed" data-id="' . $user->id . '" data-email="' . $user->email . '" data-confirmed="' . $user->confirmed . '" data-confirmation_date="' . $date . '">';
return $user->confirmed ? $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>' : $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
})
->addColumn('active', function (User $user) {
if ($user->trashed()) {
$date = Carbon::parse(now())->addDays(10)->format('d.m.Y H:i');
$email = str_replace("delete-", "", $user->email);
$link = '<a href="#" data-toggle="modal" data-target="#modals-restore" data-id="' . $user->id . '" data-email="' . $email . '" data-active="' . $user->active . '" data-payment_account="' . $date . '">';
return $link . '<span class="badge badge-pill badge-info"><i class="fa fa-undo"></i> Account reaktivieren</span></a>';
}
$date = $user->getActiveDateFormat();
$link = '<a href="#" data-toggle="modal" data-target="#modals-active" data-id="' . $user->id . '" data-email="' . $user->email . '" data-active="' . $user->active . '" data-active_date="' . $date . '">';
return $user->active ? $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>' : $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
})
->addColumn('account', function (User $user) {
$date = $user->getPaymentAccountDateFormat();
$link = '<a href="#" data-toggle="modal" data-target="#modals-account" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_account="' . $date . '">';
if ($user->payment_account) {
if ($user->isActiveAccount()) {
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>';
}
return $link . '<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> ' . $date . '</span></a>';
}
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
})
->addColumn('shop', function (User $user) {
$date = $user->getPaymentShopDateFormat();
$link = '<a href="#" data-toggle="modal" data-target="#modals-shop" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_shop="' . $date . '">';
if ($user->payment_shop) {
if ($user->isActiveShop()) {
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>';
}
return $link . '<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> ' . $date . '</span></a>';
}
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
})
->addColumn('shop_domain', function (User $user) {
return $user->shop ? '<a href="' . $user->shop->getSubdomain(false) . '" target="_blank">' . $user->shop->getSubdomain(false) . '</a>' : '';
})
->addColumn('since', function (User $user) {
if ($user->shop) {
if ($user->shop->active) {
return $user->shop->getActiveDateFormatSmall();
}
return $user->shop->getActiveDateFormatSmall();
}
return "-";
})
->addColumn('country', function (User $user) {
return ($user->account && $user->account->country) ? $user->account->country->de : '';
})
->addColumn('my_payment_methods', function (User $user) {
$payment_methods = json_encode($user->payment_methods);
$link = '<a href="#" data-toggle="modal" data-target="#modals-payment_methods" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_methods="' . htmlspecialchars($payment_methods) . '">';
if (!$user->payment_methods) {
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
}
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $user->getPaymentMethodsShort() . '</span></a>';
})
->addColumn('action_login', function (User $user) {
return '<a href="' . route('admin_user_login_as', [$user->id]) . '" class="btn icon-btn btn-sm btn-warning" onclick="return confirm(\'' . __('Login as User?') . '\');"><span class="fa fa-sign-in-alt"></span></a>';
})
->addColumn('action_delete', function (User $user) {
return '<a class="btn icon-btn btn-sm btn-danger" href="#" data-toggle="modal" data-target="#modals-user-delete" data-id="' . $user->id . '" data-email="' . $user->email . '"><span class="fa fa-trash"></span></a>';
})
->addColumn('test_mode', function (User $user) {
$link = '<a href="#" data-toggle="modal" data-target="#modals-test_mode" data-id="' . $user->id . '" data-email="' . $user->email . '" data-test_mode="' . $user->test_mode . '">';
return $user->test_mode ? $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span></a>' : $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
})
->orderColumn('id', 'id $1')
->orderColumn('email', 'email $1')
->orderColumn('confirmed', 'confirmed $1')
->orderColumn('active', 'active $1')
->orderColumn('shop', 'shop $1')
->orderColumn('admin', 'active $1')
->rawColumns(['id', 'email', 'admin', 'confirmed', 'active', 'account', 'shop', 'shop_domain', 'my_payment_methods', 'test_mode', 'action_login', 'action_delete'])
->make(true);
}
}
}

0
app/Http/Controllers/Api/AuthController.php Normal file → Executable file
View file

View file

@ -2,60 +2,96 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Product as ModelsProduct;
use Illuminate\Http\Response;
use Wearepixel\LaravelGoogleShoppingFeed\LaravelGoogleShoppingFeed;
use Vitalybaev\GoogleMerchant\Feed;
use Vitalybaev\GoogleMerchant\Product;
use Vitalybaev\GoogleMerchant\Product\Shipping;
use Vitalybaev\GoogleMerchant\Product\Availability\Availability;
use App\Services\Util;
class GoogleMerchantController extends Controller
{
public function __construct() {}
/**
* Generate Google Merchant feed
*
* @return Response
*/
public function feed()
public function __construct()
{
$products = ModelsProduct::where('active', true)->whereJsonContains('show_on', '1')->orderBy('pos', 'DESC')->get();
// Create feed object
$feed = LaravelGoogleShoppingFeed::init(
'mivita shop',
'Bio Aloe Vera & Naturkosmetik',
'https://mivita.shop'
);
// Put products to the feed
foreach ($products as $product) {
$feed->addItem([
'id' => $product->id,
'title' => $product->name,
'description' => $product->copy,
'link' => $product->getProductUrl(),
'g:image_link' => $product->getImageUrl(),
'g:availability' => 'in stock',
'g:price' => "{$product->price} EUR",
'g:brand' => 'MIVITA',
'g:gtin' => $product->ean,
'g:condition' => 'new',
'g:custom_label_0' => $product->weight,
'g:custom_label_1' => $product->contents_total,
'g:custom_label_2' => $product->getUnitType(),
'g:custom_label_3' => $product->contents_str,
'g:custom_label_4' => $product->ingredients,
'g:unit_pricing_measure' => $product->getBasePriceFormattedFullWith(false, false, null)
]);
}
return $feed->generate();
// Get the feed XML
//$feedXml = $feed->toString();
//return response($feedXml)->header('Content-Type', 'application/xml');
}
// http://api.mivita.test/google/merchant/feed
}
public function feed(){
$products = ModelsProduct::where('active', true)->whereJsonContains('show_on', '1')->orderBy('pos', 'DESC')->get();
// Create feed object
$feed = new Feed("mivita shop", "https://mivita.shop", "Bio Aloe Vera & Naturkosmetuk");
// Put products to the feed ($products - some data from database for example)
foreach ($products as $product) {
$item = new Product();
// Set common product properties
$item->setId($product->id);
$item->setTitle($product->name);
$item->setDescription($product->copy);
$item->setLink($product->getProductUrl());
$item->setImage($product->getImageUrl());
$item->setAvailability(Availability::IN_STOCK);
/*if ($product->isAvailable()) {
$item->setAvailability(Availability::IN_STOCK);
} else {
$item->setAvailability(Availability::OUT_OF_STOCK);
}*/
$item->setPrice("{$product->price} EUR");
//$item->setGoogleCategory($product->category_name);
$item->setBrand('MIVITA');
$item->setGtin($product->ean);
$item->setCondition('new');
// Some additional properties
//$item->setColor($product->color);
//$item->setSize($product->size);
// Shipping info
/*
$shipping = new Shipping();
$shipping->setCountry('US');
$shipping->setRegion('CA, NSW, 03');
$shipping->setPostalCode('94043');
$shipping->setLocationId('21137');
$shipping->setService('DHL');
$shipping->setPrice('1300 USD');
$item->setShipping($shipping);
*/
// Set a custom shipping label and weight (optional)
//$item->setShippingLabel('ups-ground');
//$item->setShippingWeight('2.14');
// Set a custom label (optional)
$item->setCustomLabel($product->weight, 'product_width');
$item->setCustomLabel($product->contents_total, 'product_contents_total');
$item->setCustomLabel($product->getUnitType(), 'product_contents_unit',);
$item->setCustomLabel($product->contents_str, 'product_contents');
$item->setCustomLabel($product->ingredients, 'product_ingredients');
$item->setCustomLabel($product->getBasePriceFormattedFullWith(false, false, null), 'product_base_pricing_unit');
//$item->setCustomLabel('Some Label 2', 1);
// Add this product to the feed
$feed->addProduct($item);
}
// Here we get complete XML of the feed, that we could write to file or send directly
$feedXml = $feed->build();
print ($feedXml);
}
// http://api.mivita.test/google/merchant/feed
}

6
app/Http/Controllers/Api/KasController.php Normal file → Executable file
View file

@ -13,7 +13,7 @@ class KasController extends Controller
// Logindaten
private $kas_user = 'w017f6e4'; // KAS-Logon
private $kas_pass = 'Medxiz-funteb-7dubdi'; // KAS-Passwort
private $kas_pass = '7mMJUF4YSVWNpp39'; // KAS-Passwort
private $session_lifetime = 600; // Gültigkeit des Tokens in Sek. bis zur neuen Authentifizierung
private $session_update_lifetime = 'Y'; // bei N läuft die Session nach <$session_lifetime> Sekunden ab, bei Y verlängert sich die Session mit jeder Benutzung
private $CredentialToken = false;
@ -56,7 +56,7 @@ class KasController extends Controller
}
// Fehler abfangen und ausgeben
catch (\SoapFault $fault)
catch (SoapFault $fault)
{
trigger_error(" Fehlernummer: {$fault->faultcode},
Fehlermeldung: {$fault->faultstring},
@ -88,7 +88,7 @@ class KasController extends Controller
}
// Fehler abfangen und ausgeben
catch (\SoapFault $fault)
catch (SoapFault $fault)
{
trigger_error("Fehlernummer: {$fault->faultcode},
Fehlermeldung: {$fault->faultstring},

201
app/Http/Controllers/Api/KasSLLController.php Normal file → Executable file
View file

@ -8,83 +8,154 @@ use App\Http\Controllers\Controller;
class KasSLLController extends Controller
{
private static $ssl_certificate_sni_csr = "";
private static $ssl_certificate_sni_csr = "-----BEGIN CERTIFICATE REQUEST-----
MIIC0DCCAbgCAQAwgYoxCzAJBgNVBAYTAkRFMQ4wDAYDVQQRDAU4Nzc1NTEPMA0G
A1UECAwGQmF5ZXJuMRUwEwYDVQQHDAxLaXJjaGhhc2xhY2gxEzARBgNVBAkMCkxl
aW5mZWxkIDIxFjAUBgNVBAoMDXJpd2EtdGVjIGUuSy4xFjAUBgNVBAMMDSoubWl2
aXRhLmNhcmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVOhtOTJBn
5V9SmHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veemfY1j
6rXq7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj86TT
Abq5wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9Q2Fb
UKEfiEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXorW6X
aifb1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1FOSc
WlN+M/+TyUybAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAJeDEZBjk9ITfZAzJ
LMVIsu4Cuz2YZkZY8r+Wdd8E1k0lAdcht2xY/uL91NwXl/hUJiVo4uBUFnCogc/k
dAxrRsrjiw8nHgfBgreGZj73S+tx00DUz1eP9uIVNzSO+aRMBHL8BvvLUR94KVSu
aVhy8fJESdDiF5TwZR7jPIWoU0esI1cEebFG2kS/wTSuUWxLh1ZGGuEKFETfEpOK
ooy0gUcHTP1NWo/vTDwdlf47t2vvZ/ZD0ursWXp6CNNZvwimHPxgSq8KKxLQyf5U
S/UHogxC8PbOzTJI0DutkCZO0iUO8gTq0GXZHVqkqTCixfIFeuMuL0ZvXYJVhZXP
4CBn5g==
-----END CERTIFICATE REQUEST-----";
private static $ssl_certificate_sni_key = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgohGr2e3ysw/Awvzh
qkqDS4iQgRvWwNIYxTcPxpdcndGhRANCAASZjlV2bQbLQrOveMlYOowR3IlfND7z
OxauFGabhvWSU1cg2w4U4bu/QXnDXfHHkcLp4M5WgHzX9Nw2m/abyJJ6
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVOhtOTJBn5V9S
mHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veemfY1j6rXq
7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj86TTAbq5
wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9Q2FbUKEf
iEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXorW6Xaifb
1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1FOScWlN+
M/+TyUybAgMBAAECggEAJ0hYj9AP44m6AiApRpbCdPiLhZmx3ANfrOJpi1dc2BqD
pIzCePOXlnh+6fMV0Cn7uY60QFuksLzEjsdBXLtgQYvuGu1plSZT/5VAA4RnhYpJ
7O+tnvFt00k/iCi/bWmCXY4kCvrEVNeLtALoa9znOVMhiBtGGiFxO3iQ+y7jxF6J
49O99G8gPGjMm/BdFjnBpUZ+Z5ZGXvrKTZaQRDE5HXEM8dUTBXPL4+dMdfQIiyKZ
pNklwkMjS4/LY6xDP16Wj25bSq5W9WSlTja/ZJ2eKqr6c7WxKP6TvjGh9FMkIUps
Bl9BNKmgixgiHVq/4WwUSZ1PAEuGQJiptVdeJcgioQKBgQDDdNaRg6Z5yVk+UjXw
DHJkUmquowijJUG/2seLYMFm1lkr9xbGvfGfnOSr79jim3haL/qichWh++QjeBsM
fwBPMbRY+JNMHpaDpvHAI2YNqXP+rBr4pJnICrHoqIzVqxbDJ04LQZBRD10cTlFz
+l+Ok60XTAX/wlKN96BnjuOVXQKBgQDDc2aoU37E4wPYNXcMLvoDv3+Zq3KCEMQD
gtNgSbyd37Dw8n35TGWubFLsvYnPLBebB6wAgTPzvTpJmPTr7nKUJsd4rbfvuh+i
vVhH/2xq70Pi1XqvQkmo+H1OJX+t2n/Hxr7TQGkqVI9eNfvW8UP+TGPjxGIw8Y0b
6t8Ky6USVwKBgQCszV5qVh9Xqtj4zUwch5SW93qUHVWkj2rayP0ET62NUtKRmSmM
2h+GAvr0u99fMR6tdZ+8AOr5RC7F4Qjg+mN2oLYWtuXbNWvSx0USnvk5+Oexb82E
qFnBTxtNW77vpQxByz0nnHaQA+pI/UDsLZ5P+mXco/zlypKcKyKoi97PjQKBgDQV
9+CZx6m+edLPhLc5eaUwDlgsaWqh/yqUXbJGVD6aUzQS22Fpa5uNAJhYdnZAYNYO
uFa2F9s3rWXZnkOVmvFCWFwfp2n6Zt3eqb0eI41nz+aOT5CPEMQ33GTL93ekR/M8
UrRHcP8347EOn9uLFjyZrPEQ773tUVaERAZDeO0nAoGAZXMhlmKmqTrM2jSb64ja
pEddcEW2LuTvwQueOKUuSSwmCydKXkcgrYZ4EHyOgvVN9JZ5ZfW6ZathFipVEKdy
diQ860kC4h++erAa8dvB1DUG5oldYYPiEKOyyyn+tNU298QcEkLrG1JcLuUXpfTg
8dPIr+VpGomsvpwGTfJFjlE=
-----END PRIVATE KEY-----";
private static $ssl_certificate_sni_crt = "-----BEGIN CERTIFICATE-----
MIIEpDCCBEqgAwIBAgIQVIm0T0SQ6D20YQxMaHEKbDAKBggqhkjOPQQDAjCBjzEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5T
ZWN0aWdvIEVDQyBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMB4X
DTI0MDgwMTAwMDAwMFoXDTI1MDkwMTIzNTk1OVowGDEWMBQGA1UEAwwNKi5taXZp
dGEuY2FyZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJmOVXZtBstCs694yVg6
jBHciV80PvM7Fq4UZpuG9ZJTVyDbDhThu79BecNd8ceRwungzlaAfNf03Dab9pvI
knqjggL8MIIC+DAfBgNVHSMEGDAWgBT2hQo7EYbhBH0Oqgss0u7MZHt7rjAdBgNV
HQ4EFgQUVCkHH2AasJQFWFs63rdcb6BRvyowDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
EwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARC
MEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdv
LmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4MHYwTwYIKwYBBQUHMAKG
Q2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb0VDQ0RvbWFpblZhbGlkYXRp
b25TZWN1cmVTZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl
Y3RpZ28uY29tMCUGA1UdEQQeMByCDSoubWl2aXRhLmNhcmWCC21pdml0YS5jYXJl
MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDd3Mo0ldfhFgXnlTL6x5/4PRxQ
39sAOhQSdgosrLvIKgAAAZEMky0iAAAEAwBHMEUCICSH9TLHP8tqMyBTBpxF1+lw
4wAnWf4E5pPJ6651S8P9AiEAkKqOQDaVdoFI1+jM28grXnG5o0vFLUwa0o49KYQ3
k+sAdgAN4fIwK9MNwUBiEgnqVS78R3R8sdfpMO8OQh60fk6qNAAAAZEMkyzbAAAE
AwBHMEUCIFJfJS4cojUm9nHQ1TVlxpFwOV7QwCj9MOfq0CCkVzsGAiEA8WQrE1ri
kJkeIVPSgUVJpIz8TKef2aR+Ivzkzon52QIAdgAS8U40vVNyTIQGGcOPP3oT+Oe1
YoeInG0wBYTr5YYmOgAAAZEMkyzBAAAEAwBHMEUCIQCH8/qTmCNea3FdBVk0c3Wu
FrvYnoQlTQaaDS/zeTxSzwIge6VO5Aeor30Wu675zBYzNsIru5gXOTl4dteBMYnC
0JswCgYIKoZIzj0EAwIDSAAwRQIhAKxmgpPqW6UAcWHCoWAPN673pBMxnCKn3vFq
wUkhGrT7AiBDUsDuMhabsGlZ10X2GXcm+1mwxdMLSDYEWiwk5fUaNA==
MIIGMTCCBRmgAwIBAgIRANRDAE1KIec3seJ9ed+Qy4UwDQYJKoZIhvcNAQELBQAw
gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
QTAeFw0yMjA3MTIwMDAwMDBaFw0yMzA4MTIyMzU5NTlaMBgxFjAUBgNVBAMMDSou
bWl2aXRhLmNhcmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVOhtO
TJBn5V9SmHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veem
fY1j6rXq7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj
86TTAbq5wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9
Q2FbUKEfiEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXo
rW6Xaifb1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1
FOScWlN+M/+TyUybAgMBAAGjggL8MIIC+DAfBgNVHSMEGDAWgBSNjF7EVK2K4Xfp
m/mbBeG4AY1h4TAdBgNVHQ4EFgQUCS0Y1v7p19isO7cTuP3YrKVr2OcwDgYDVR0P
AQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYX
aHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4
MHYwTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1JT
QURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGG
F2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCUGA1UdEQQeMByCDSoubWl2aXRhLmNh
cmWCC21pdml0YS5jYXJlMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdQCt9776
fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAYHxRovYAAAEAwBGMEQCIA3z
UR5BFV7bwBcdRhS8mru20uq36DNz3ILivZh9yl4CAiAUjDxqZBW0Po/0Rm0gumI1
VBZfqMSDiA7Cr1peGN8B8wB3AHoyjFTYty22IOo44FIe6YQWcDIThU070ivBOlej
UutSAAABgfFGi6UAAAQDAEgwRgIhAN9s3/v2ygh1tfPQ8iX2dLZdOVxyuvC7bf15
KP4NQyabAiEAz88hRBxRu3FifpLaYNjwxy1fRUc2luWfDdw+f31TOfoAdgDoPtDa
PvUGNTLnVyi8iWvJA9PL0RFr7Otp4Xd9bQa9bgAAAYHxRot7AAAEAwBHMEUCIQCQ
RYqkeiRlStQacqUjuw5435NqNDqDlydAVYgywX05pwIgdcFtcLMGevO/KRyUeuWw
8hYY6y1S7VYVdcFIuZVp5mswDQYJKoZIhvcNAQELBQADggEBAA0eEYSKcbgEPczo
ABXpVsfbmaZqPhAKqcqKeGUcmFo7JHVPRUyck8RAF+SravyaHhilygU727QG4oUt
riCewV39cKD2m7CO24WHe5+Fw8eslsJE+DBq/2WpLRJIGSWLl1r7WUELKQhqEYkr
DCpkDXpG+lsDIfc5DC4dPLSWc9ezObsS4KEMCMDw+bj5GMGV6dHQZxAnbyqi71+v
4+AOHpcYfe6v63w82M0YN5oTnaOukLVDgMXJ7WZP2op7atojB7DeM7k3+fj79kVJ
jUGlvHLdN9jsczEZGGxL3w5oIjC4HMK1U5kyzEFWpc8ZLg+YPvF8w15lbhIXG94l
JscmCFU=
-----END CERTIFICATE-----";
private static $ssl_certificate_sni_bundle = "-----BEGIN CERTIFICATE-----
MIIDqDCCAy6gAwIBAgIRAPNkTmtuAFAjfglGvXvh9R0wCgYIKoZIzj0EAwMwgYgx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJz
ZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQD
EyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE4MTEw
MjAwMDAwMFoXDTMwMTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAkdCMRswGQYDVQQI
ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT
D1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGlnbyBFQ0MgRG9tYWluIFZh
bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABHkYk8qfbZ5sVwAjBTcLXw9YWsTef1Wj6R7W2SUKiKAgSh16TwUwimNJE4xk
IQeV/To14UrOkPAY9z2vaKb71EijggFuMIIBajAfBgNVHSMEGDAWgBQ64QmG1M8Z
wpZ2dEl23OA1xmNjmjAdBgNVHQ4EFgQU9oUKOxGG4QR9DqoLLNLuzGR7e64wDgYD
VR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEwUAYD
VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz
dEVDQ0NlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/
BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdEVD
Q0FkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1
c3QuY29tMAoGCCqGSM49BAMDA2gAMGUCMEvnx3FcsVwJbZpCYF9z6fDWJtS1UVRs
cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9
4ToKFbZl+/OnFFzqnvOhcjHvClECEQcKmc8fmA==
MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx
MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV
BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE
ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g
VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N
TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj
eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E
oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk
Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY
uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j
BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb
+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw
CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0
LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr
BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv
bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov
L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H
ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH
7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi
H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx
RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv
xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38
sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL
l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq
6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY
LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5
yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K
00u/I5sUKUErmgQfky3xxzlIPK1aEn8=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7
MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0
aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL
q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc
JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA
FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1
xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI
MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j
b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM
BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy
ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+
FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV
bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY
CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf
8qn0dNW44bOwgeThpWOjzOoEeJBuv/c=
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI
s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG
vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ
Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb
IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0
tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E
xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV
icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5
D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ
WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ
5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG
KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg
EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID
ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG
BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t
L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr
BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+
rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+
/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA
CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F
zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA
vGp4z7h/jnZymQyd/teRCBaho1+V
-----END CERTIFICATE-----";

235
app/Http/Controllers/Api/PayoneController.php Normal file → Executable file
View file

@ -2,26 +2,32 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PaymentTransaction;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
use App\Services\MyLog;
use App\Services\Payment;
use App\Services\ShoppingUserService;
use App\Services\Shop;
use App\Services\Util;
class PayoneController extends Controller
{
public function __construct() {}
public function paymentStatus()
public function __construct()
{
}
public function paymentStatus(){
$data = \Request::all();
// test para
/* $data = [
/* $data = [
'key' => '698fb2555f8b2efc74f60b2121421f45',
'txaction' => 'paid',
'clearingtype' => 'wlt',
@ -34,114 +40,71 @@ class PayoneController extends Controller
*/
if (! isset($data['key']) || ! isset($data['param']) || ! isset($data['userid']) || ! isset($data['txid']) || ! isset($data['reference']) || ! isset($data['price'])) {
MyLog::writeLog(
'payone',
'error',
'Error:2001 App\Http\Controllers\Api\PayoneController::paymentStatus parameter incomplete',
$data
);
echo 'TSOK';
exit;
if(!isset($data['key']) || !isset($data['param']) || !isset($data['userid']) || !isset($data['txid']) || !isset($data['reference']) || !isset($data['price'])){
\Log::channel('payone')->error('PaymentStatus: parameter incomplete: '.json_encode($data));
echo "PaymentStatus: parameter incomplete:";
var_dump($data);
die();
}
if ($data['key'] != config('payone.defaults.key')) {
MyLog::writeLog(
'payone',
'error',
'Error:2002 App\Http\Controllers\Api\PayoneController::paymentStatus Key error',
$data
);
echo 'TSOK';
exit;
if($data['key'] != config('payone.defaults.key')) {
\Log::channel('payone')->error('PaymentStatus: Key error: '.json_encode($data));
echo "PaymentStatus: Key error:";
var_dump($data);
die();
}
$shopping_order = ShoppingOrder::find($data['param']);
if (! $shopping_order) {
MyLog::writeLog(
'payone',
'error',
'Error:2003 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingOrder not found:',
$data
);
echo 'TSOK';
exit;
if(!$shopping_order){
\Log::channel('payone')->error('PaymentStatus: ShoppingOrder not found: '.json_encode($data));
echo "PaymentStatus: ShoppingOrder not found:";
var_dump($data);
die();
}
$shopping_payment = ShoppingPayment::where('reference', $data['reference'])->first();
if (! $shopping_payment) {
MyLog::writeLog(
'payone',
'error',
'Error:2004 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingPayment not found',
$data
);
echo 'TSOK';
exit;
if(!$shopping_payment){
\Log::channel('payone')->error('PaymentStatus: ShoppingPayment not found: '.json_encode($data));
echo "PaymentStatus: ShoppingPayment not found:";
var_dump($data);
die();
}
if ($shopping_payment->shopping_order_id != $shopping_order->id) {
MyLog::writeLog(
'payone',
'error',
'Error:2005 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingPayment no realation ShoppingOrder',
$data
);
echo 'TSOK';
exit;
if($shopping_payment->shopping_order_id != $shopping_order->id){
\Log::channel('payone')->error('PaymentStatus: ShoppingPayment no realation ShoppingOrder: '.json_encode($data));
echo "PaymentStatus: ShoppingPayment no realation ShoppingOrder:";
var_dump($data);
die();
}
$price = number_format((round($data['price'], 2) * 100), 0, '.', '');
if($data['key'] != config('payone.defaults.key')) {
\Log::channel('payone')->error('PaymentStatus: Key error: '.json_encode($data));
echo "PaymentStatus: ShoppingPayment no realation ShoppingOrder:";
var_dump($data);
die();
}
$price = number_format((round($data['price'],2) * 100), 0, '.', '');
$price_amount = number_format($shopping_payment->amount, 0, '.', '');
if ($price_amount != $price) {
if($price_amount != $price){
$data['shopping_payment-amount'] = $price_amount;
$data['price-amount'] = $price;
MyLog::writeLog(
'payone',
'error',
'Error:2006 App\Http\Controllers\Api\PayoneController::paymentStatus Price error',
$data
);
echo 'TSOK';
exit;
}
/*
* Payone sendet dieselbe txaction oft mehrfach (v. a. "appointed"). War der Status
* bereits auf ShoppingPayment gespeichert, ist das ein Duplikat: TSOK, keine Doppel-Verarbeitung.
* Ausnahme: erneutes "paid", obwohl die Bestellung noch nicht als bezahlt gefuehrt wird (Recovery).
*/
if ($shopping_payment->txaction == $data['txaction']) {
if ($data['txaction'] === 'paid' && $shopping_order->txaction === 'paid') {
MyLog::writeLog(
'payone',
'notice',
'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (already paid)',
$data,
false
);
echo 'TSOK';
exit;
}
if (in_array($data['txaction'], ['appointed', 'failed', 'pending'], true)) {
MyLog::writeLog(
'payone',
'info',
'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (same txaction)',
[
'reference' => $data['reference'] ?? null,
'param' => $data['param'] ?? null,
'txaction' => $data['txaction'],
'txid' => $data['txid'] ?? null,
],
false
);
echo 'TSOK';
exit;
}
\Log::channel('payone')->error('PaymentStatus: Price error: '.json_encode($data));
echo "PaymentStatus: Price error:";
var_dump($data);
die();
}
// create transaction
/* TODO -- need this?
if($shopping_payment->txaction == $data['txaction']){
\Log::channel('payone')->error('PaymentStatus: same txaction error: '.json_encode($data));
echo "PaymentStatus: same txaction:";
var_dump($data);
die();
}
*/
//create transaction
PaymentTransaction::create([
'shopping_payment_id' => $shopping_payment->id,
'request' => 'transaction',
@ -154,78 +117,30 @@ class PayoneController extends Controller
'mode' => $data['mode'],
]);
// Define txaction priority (higher number = higher priority)
$txaction_priority = [
'appointed' => 1,
'pending' => 2,
'failed' => 3,
'paid' => 10, // highest priority - final state
];
$current_priority = isset($txaction_priority[$shopping_order->txaction]) ? $txaction_priority[$shopping_order->txaction] : 0;
$new_priority = isset($txaction_priority[$data['txaction']]) ? $txaction_priority[$data['txaction']] : 0;
// Only update txaction if new priority is higher than current
if ($new_priority > $current_priority) {
$shopping_order->txaction = $data['txaction'];
$shopping_order->save();
$shopping_payment->txaction = $data['txaction'];
$shopping_payment->save();
} else {
MyLog::writeLog(
'payone',
'info',
'App\Http\Controllers\Api\PayoneController::paymentStatus - txaction not updated (current: '.$shopping_order->txaction.' has higher/equal priority than new: '.$data['txaction'].')',
$data,
false
);
}
$shopping_order->txaction = $data['txaction'];
$shopping_order->save();
$shopping_payment->txaction = $data['txaction'];
$shopping_payment->save();
$send_link = false;
$send_mail = true;
if ($data['txaction'] === 'failed') {
if($data['txaction'] === 'failed'){
$shopping_order->setUserHistoryValue(['status' => 6]);
Util::setInstanceStatusByPayment($shopping_payment, 5);
}
if ($data['txaction'] === 'appointed') {
if($data['txaction'] === 'appointed'){
$shopping_order->setUserHistoryValue(['status' => 7]);
ShoppingUserService::snycOrdersByShoppingOrder($shopping_order);
Util::setInstanceStatusByPayment($shopping_payment, 4);
Shop::userOrders();
}
if ($data['txaction'] === 'paid') {
// Use DB transaction and row locking to prevent race conditions
\DB::beginTransaction();
try {
// Lock the shopping order row to prevent concurrent processing
$locked_order = ShoppingOrder::where('id', $shopping_order->id)
->lockForUpdate()
->first();
// Double-check if payment was already processed
if (! $locked_order->paid) {
$send_link = Payment::paymentStatusPaidAction($locked_order, true, $shopping_payment);
\DB::commit();
} else {
$send_mail = false;
\DB::commit();
}
} catch (\Exception $e) {
\DB::rollBack();
MyLog::writeLog(
'payone',
'error',
'Error:2008 App\Http\Controllers\Api\PayoneController::paymentStatus Transaction failed',
['error' => $e->getMessage(), 'data' => $data]
);
throw $e;
}
if($data['txaction'] === 'paid'){
$send_link = Payment::paymentStatusPaidAction($shopping_order, true);
}
$data['send_link'] = $send_link;
if ($send_mail) {
Payment::paymentStatusSendMail($shopping_order, $shopping_payment, $data);
}
echo 'TSOK';
Payment::paymentStatusSendMail($shopping_order, $shopping_payment, $data);
print("TSOK");
exit;
}
}
}

404
app/Http/Controllers/Api/ShoppingUserController.php Normal file → Executable file
View file

@ -2,7 +2,6 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Mail\MailCheckout;
use App\Models\Country;
use App\Models\Product;
@ -11,23 +10,28 @@ use App\Models\ShoppingOrder;
use App\Models\ShoppingOrderItem;
use App\Models\ShoppingUser;
use App\Services\CustomerPriority;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use PHPUnit\Framework\Constraint\Count;
use Yard;
class ShoppingUserController extends Controller
{
// protected static API_MAIL = 'api.thomas.krummel@gmail.com';
// protected static API_PASS = 'UF(Q<9knap!ev3vH?5~!b8DP';
//protected static API_MAIL = 'api.thomas.krummel@gmail.com';
//protected static API_PASS = 'UF(Q<9knap!ev3vH?5~!b8DP';
protected $successStatus = 200;
protected $member_id = 3; //service@aloe-vera.bio
protected $member_id = 3; // service@aloe-vera.bio
/**
* @param Request $request
* wp_order_numbers[1234, 1234]
* @param Request $request
* wp_order_numbers[1234, 1234]
* @return \Illuminate\Http\JsonResponse
*/
public function status(Request $request)
@ -37,23 +41,23 @@ class ShoppingUserController extends Controller
'wp_order_numbers' => 'required',
]);
if (! is_array($request->wp_order_numbers)) {
if(!is_array($request->wp_order_numbers)){
$wp_order_numbers = json_decode($request->wp_order_numbers);
} else {
$wp_order_numbers = $request->wp_order_numbers;
}else{
$wp_order_numbers = $request->wp_order_numbers;
}
if (! $wp_order_numbers || ! is_array($wp_order_numbers)) {
if(!$wp_order_numbers || !is_array($wp_order_numbers)){
return response()->json([
'success' => false,
'message' => 'wp_order_numbers need as json [1234, 1234] ',
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
$status = [];
foreach ($wp_order_numbers as $wp_order_number) {
foreach ($wp_order_numbers as $wp_order_number){
$shopping_user = ShoppingUser::where('wp_order_number', '=', $wp_order_number)->first();
$status[] = [
'wp_order_number' => $wp_order_number,
@ -66,14 +70,14 @@ class ShoppingUserController extends Controller
return response()->json([
'success' => true,
'data' => $status,
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
/**
* @param Request $request
* wp_order_number [1234]
* @param Request $request
* wp_order_number [1234]
* @return \Illuminate\Http\JsonResponse
*/
public function cancel(Request $request)
@ -82,31 +86,31 @@ class ShoppingUserController extends Controller
'wp_order_number' => 'required|int',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->first();
if (! $shopping_user) {
if (!$shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' not found',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' not found',
'order' => false,
'status' => false,
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if (! $shopping_user->shopping_order) {
if(!$shopping_user->shopping_order){
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' has no order',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' has no order',
'order' => false,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if ($shopping_user->shopping_order->shipped > 0) {
if($shopping_user->shopping_order->shipped > 0){
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' can not cancel',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' can not cancel',
'order' => true,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
@ -115,17 +119,18 @@ class ShoppingUserController extends Controller
return response()->json([
'success' => true,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' is cancel',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' is cancel',
'order' => true,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
/**
* @param Request $request
* wp_order_number [1234]
* @param Request $request
* wp_order_number [1234]
* @return \Illuminate\Http\JsonResponse
*/
public function open(Request $request)
@ -134,31 +139,31 @@ class ShoppingUserController extends Controller
'wp_order_number' => 'required|int',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->first();
if (! $shopping_user) {
if (!$shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' not found',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' not found',
'order' => false,
'status' => false,
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if (! $shopping_user->shopping_order) {
if(!$shopping_user->shopping_order){
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' has no order',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' has no order',
'order' => false,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if ($shopping_user->shopping_order->shipped !== 10) {
if($shopping_user->shopping_order->shipped !== 10){
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' can not open',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' can not open',
'order' => true,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
@ -167,44 +172,45 @@ class ShoppingUserController extends Controller
return response()->json([
'success' => true,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' is open',
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' is open',
'order' => true,
'status' => $shopping_user->getAPIShippedType(),
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
/**
* @param Request $request
* wp_order_numbers [1234, 1234]
* @param Request $request
* wp_order_numbers [1234, 1234]
* @return \Illuminate\Http\JsonResponse
*/
public function show(Request $request)
{
// $this->member_id = auth()->user()->m_sponsor;
//$this->member_id = auth()->user()->m_sponsor;
$request->validate([
'wp_order_numbers' => 'required',
]);
if (! is_array($request->wp_order_numbers)) {
if(!is_array($request->wp_order_numbers)){
$wp_order_numbers = json_decode($request->wp_order_numbers);
} else {
$wp_order_numbers = $request->wp_order_numbers;
}else{
$wp_order_numbers = $request->wp_order_numbers;
}
if (! $wp_order_numbers || ! is_array($wp_order_numbers)) {
if(!$wp_order_numbers || !is_array($wp_order_numbers)){
return response()->json([
'success' => false,
'message' => 'wp_order_numbers need as json [1234, 1234] ',
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 400);
}
$data = [];
foreach ($wp_order_numbers as $wp_order_number) {
$shopping_user = ShoppingUser::where('wp_order_number', '=', $wp_order_number)->first();
foreach ($wp_order_numbers as $wp_order_number){
$shopping_user = ShoppingUser::where('wp_order_number', '=', $wp_order_number)->first();
$user = false;
$order = false;
if ($shopping_user) {
@ -214,21 +220,21 @@ class ShoppingUserController extends Controller
$data[] = [
'wp_order_number' => $wp_order_number,
'user' => $user,
'order' => $order,
'order' => $order,
'customer_number' => $shopping_user ? $shopping_user->number : false,
'member_email' => ($shopping_user && $shopping_user->member) ? $shopping_user->member->email : false,
'status' => $shopping_user ? $shopping_user->getAPIShippedType() : false, ];
}
return response()->json([
'success' => true,
'data' => $data,
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
@ -248,20 +254,19 @@ class ShoppingUserController extends Controller
$this->member_id = auth()->user()->m_sponsor;
$data = $this->prepareForStore($request->all());
$data['member_id'] = $this->member_id;
$data['member_id'] = $this->member_id ;
$data['number'] = ShoppingUser::max('number') + 1;
$data['mode'] = $request->mode ? $request->mode : 'live';
$data['is_from'] = 'extern';
$data['is_for'] = 'ot-member';
$data['is_for'] = 'ot';
$shopping_user = ShoppingUser::create($data);
// Kundenhoheit prüfen
//Kundenhoheit prüfen
$priority = CustomerPriority::checkOne($shopping_user, true, false, true);
\App\Services\Shop::newUserOrder($shopping_user->number);
// exists //like //update
//exists //like //update
$user = $this->prepareForShow($shopping_user);
return response()->json([
'success' => true,
'data' => [
@ -271,12 +276,13 @@ class ShoppingUserController extends Controller
'customer_number' => $shopping_user->number,
'member_email' => ($shopping_user && $shopping_user->member) ? $shopping_user->member->email : false,
],
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function update(Request $request)
@ -285,42 +291,40 @@ class ShoppingUserController extends Controller
'wp_order_number' => 'required|int',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->first();
if (! $shopping_user) {
if (!$shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' not found',
'time' => Carbon::now()->toDateTimeString(),
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' not found',
'time' => Carbon::now()->toDateTimeString()
], 400);
}
$data = $this->prepareForUpdate($request->all());
// Kundenhoheit prüfen
//Kundenhoheit prüfen
$priority = CustomerPriority::checkChangeOne($shopping_user, $data, true);
$updated = $shopping_user->fill($data)->save();
\App\Services\Shop::newUserOrder($shopping_user->number);
if ($updated) {
if ($updated){
$user = $this->prepareForShow($shopping_user);
$order = $this->prepareForShowOrder($shopping_user->shopping_order);
return response()->json([
return response()->json([
'success' => true,
'data' => [
'wp_order_number' => $shopping_user->wp_order_number,
'user' => $user,
'order' => $order,
'order' => $order,
'customer_priority' => $priority,
'customer_number' => $shopping_user ? $shopping_user->number : false,
'member_email' => ($shopping_user && $shopping_user->member) ? $shopping_user->member->email : false,
'status' => $shopping_user ? $shopping_user->getAPIShippedType() : false,
],
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
return response()->json([
'success' => false,
'message' => 'Entry could not be updated',
'message' => 'Entry could not be updated'
], 500);
}
@ -331,27 +335,28 @@ class ShoppingUserController extends Controller
'wp_order' => 'required',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->first();
if (! $shopping_user) {
if (!$shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' not found',
'time' => Carbon::now()->toDateTimeString(),
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' not found',
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if ($shopping_user->shopping_order) {
if($shopping_user->shopping_order){
return response()->json([
'success' => false,
'message' => 'Order with wp_order_number '.$request->wp_order_number.' exists',
'time' => Carbon::now()->toDateTimeString(),
'message' => 'Order with wp_order_number ' . $request->wp_order_number . ' exists',
'time' => Carbon::now()->toDateTimeString()
], 400);
}
if (! is_array($request->wp_order)) {
if(!is_array($request->wp_order)){
$wp_order = json_decode($request->wp_order);
} else {
$wp_order = $request->wp_order;
}else{
$wp_order = $request->wp_order;
}
$wp_invoice_path = isset($request->wp_invoice_path) ? $request->wp_invoice_path : null;
@ -365,10 +370,9 @@ class ShoppingUserController extends Controller
$wp_order = $this->prepareOrder($wp_order, $shopping_user, $wp_invoice_path, $api_notice);
if ($wp_order) {
if ($wp_order){
$user = $this->prepareForShow($shopping_user);
$order = $this->prepareForShowOrder($shopping_user->shopping_order);
return response()->json([
'success' => true,
'data' => [
@ -383,175 +387,166 @@ class ShoppingUserController extends Controller
'member_email' => ($shopping_user && $shopping_user->member) ? $shopping_user->member->email : false,
'status' => $shopping_user->getAPIShippedType(),
],
'time' => Carbon::now()->toDateTimeString(),
'time' => Carbon::now()->toDateTimeString()
], 200);
}
return response()->json([
'success' => false,
'message' => 'Order could not be stored',
'message' => 'Order could not be stored'
], 500);
}
public function delete(Request $request)
{
$request->validate([
'wp_order_number' => 'required|int',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->where('mode', '=', 'dev')->first();
if (! $shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number '.$request->wp_order_number.' not found or mode != dev',
'time' => Carbon::now()->toDateTimeString(),
], 400);
}
$shopping_order = $shopping_user->shopping_order;
if ($shopping_order) {
foreach ($shopping_order->shopping_order_items as $shopping_order_item) {
$shopping_order_item->delete();
}
$shopping_order->delete();
}
$shopping_user->wp_order_number = time();
$shopping_user->save();
if ($shopping_user->delete()) {
return response()->json([
'success' => true,
]);
}
{
$request->validate([
'wp_order_number' => 'required|int',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->where('mode', '=', 'dev')->first();
if (!$shopping_user) {
return response()->json([
'success' => false,
'message' => 'Entry with wp_order_number ' . $request->wp_order_number . ' not found or mode != dev',
'time' => Carbon::now()->toDateTimeString()
], 400);
}
$shopping_order = $shopping_user->shopping_order;
if($shopping_order){
foreach ($shopping_order->shopping_order_items as $shopping_order_item){
$shopping_order_item->delete();
}
$shopping_order->delete();
}
$shopping_user->wp_order_number = time();
$shopping_user->save();
if ($shopping_user->delete()) {
return response()->json([
'success' => true
]);
}
return response()->json([
'success' => false,
'message' => 'Entry could not be deleted'
], 500);
}
return response()->json([
'success' => false,
'message' => 'Entry could not be deleted',
], 500);
}
private function prepareForShow($shopping_user){
private function prepareForShow($shopping_user)
{
if (! $shopping_user) {
if(!$shopping_user){
return false;
}
$shopping_user_data = $shopping_user->toArray();
$needs = ['wp_order_number', 'wp_order_date', 'billing_company', 'billing_firstname', 'billing_lastname', 'billing_address', 'billing_address_2', 'billing_zipcode', 'billing_city', 'billing_phone', 'billing_email',
'same_as_billing', 'shipping_company', 'shipping_firstname', 'shipping_lastname', 'shipping_address', 'shipping_address_2', 'shipping_zipcode', 'shipping_city', 'shipping_phone',
'created_at', 'updated_at', 'user_deleted_at']; // 'has_buyed', 'subscribed',
'created_at', 'updated_at', 'user_deleted_at']; //'has_buyed', 'subscribed',
// $salutation = array('mr' => 1, 'ms' => 2);
//$salutation = array('mr' => 1, 'ms' => 2);
$ret = [];
foreach ($shopping_user_data as $key => $value) {
foreach ($shopping_user_data as $key=>$value){
if ($key === 'billing_country_id') {
if($key === 'billing_country_id'){
$ret['billing_country_code'] = $shopping_user->billing_country_id ? $shopping_user->billing_country->code : null;
}
if ($key === 'shipping_country_id') {
if($key === 'shipping_country_id'){
$ret['shipping_country_code'] = $shopping_user->shipping_country_id ? $shopping_user->shipping_country->code : null;
}
if ($key === 'billing_salutation') {
if($key === 'billing_salutation'){
$ret['billing_salutation'] = $shopping_user->billing_salutation === 'ms' ? 2 : 1;
}
if ($key === 'shipping_salutation') {
if($key === 'shipping_salutation'){
$ret['shipping_salutation'] = $shopping_user->shipping_salutation === 'ms' ? 2 : 1;
}
if (in_array($key, $needs)) {
if(in_array($key, $needs)){
$ret[$key] = $value;
}
}
return $ret;
}
private function prepareForShowOrder($shopping_order)
{
private function prepareForShowOrder($shopping_order){
if (! $shopping_order) {
if(!$shopping_order){
return false;
}
$ret = [
'country' => isset($shopping_order->shipping_country->country->code) ? $shopping_order->shipping_country->country->code : '',
'wp_invoice_path' => $shopping_order->wp_invoice_path,
'total' => ($shopping_order->total * 100),
'shipping' => ($shopping_order->shipping * 100),
'total_net' => ($shopping_order->subtotal * 100),
'tax_rate' => ($shopping_order->tax_rate * 100),
'tax' => ($shopping_order->tax * 100),
'total_with_shipping' => ($shopping_order->total_shipping * 100),
'total' => ($shopping_order->total*100),
'shipping' => ($shopping_order->shipping*100),
'total_net' => ($shopping_order->subtotal*100),
'tax_rate' => ($shopping_order->tax_rate*100),
'tax' => ($shopping_order->tax*100),
'total_with_shipping' => ($shopping_order->total_shipping*100),
'weight' => $shopping_order->weight,
];
$ret['items'] = [];
foreach ($shopping_order->shopping_order_items as $item) {
foreach ($shopping_order->shopping_order_items as $item){
$ret['items'][] = [
'article' => $item->product->wp_number,
'name' => $item->product->getLang('name'),
'qty' => $item->qty,
'price' => ($item->price * 100),
'article' => $item->product->wp_number,
'name' => $item->product->name,
'qty' => $item->qty,
'price' => ($item->price * 100),
];
}
return $ret;
}
private function prepareForUpdate($data)
{
private function prepareForUpdate($data){
// $salutation = array(1 => 'mr', 2 => 'ms', 3=>null);
//$salutation = array(1 => 'mr', 2 => 'ms', 3=>null);
if (isset($data['billing_salutation'])) {
$data['billing_salutation'] = (int) $data['billing_salutation'];
if(isset($data['billing_salutation'])){
$data['billing_salutation'] = (int) $data['billing_salutation'];
$data['billing_salutation'] = $data['billing_salutation'] == 2 ? 'ms' : 'mr';
}
if (isset($data['shipping_salutation'])) {
$data['shipping_salutation'] = (int) $data['shipping_salutation'];
$data['shipping_salutation'] = $data['shipping_salutation'] == 2 ? 'ms' : 'mr';
if(isset($data['shipping_salutation'])){
$data['shipping_salutation'] = (int) $data['shipping_salutation'];
$data['shipping_salutation'] = $data['shipping_salutation'] == 2 ? 'ms' : 'mr';
}
$ret = [];
$needs = ['billing_salutation', 'billing_company', 'billing_firstname', 'billing_lastname', 'billing_address', 'billing_address_2', 'billing_zipcode', 'billing_city', 'billing_phone', 'billing_email', 'same_as_billing',
$needs = [ 'billing_salutation', 'billing_company', 'billing_firstname', 'billing_lastname', 'billing_address', 'billing_address_2', 'billing_zipcode', 'billing_city', 'billing_phone', 'billing_email', 'same_as_billing',
'shipping_salutation', 'shipping_company', 'shipping_firstname', 'shipping_lastname', 'shipping_address', 'shipping_address_2', 'shipping_zipcode', 'shipping_city', 'shipping_phone'];
foreach ($data as $key => $value) {
if ($key === 'billing_country_code' && isset($data['billing_country_code'])) {
$ret['billing_country_id'] = Country::getCountryIdByCodeOrOne($data['billing_country_code']);
foreach ($data as $key=>$value){
if($key === 'billing_country_code' && isset($data['billing_country_code'])) {
$ret['billing_country_id'] = Country::getCountryIdByCodeOrOne($data['billing_country_code']);
}
if ($key === 'shipping_country_code' && isset($data['shipping_country_code'])) {
if($key === 'shipping_country_code' && isset($data['shipping_country_code']) ) {
$ret['shipping_country_id'] = Country::getCountryIdByCodeOrOne($data['shipping_country_code']);
}
if ($key === 'billing_phone') {
if($key === 'billing_phone') {
$ret['billing_phone'] = strlen($data['billing_phone']) <= 3 ? '' : $data['billing_phone'];
}
if ($key === 'shipping_phone') {
if($key === 'shipping_phone') {
$ret['shipping_phone'] = strlen($data['shipping_phone']) <= 3 ? '' : $data['shipping_phone'];
}
if (in_array($key, $needs)) {
if(in_array($key, $needs)){
$ret[$key] = $value;
}
}
return $ret;
}
private function prepareForStore($data)
{
private function prepareForStore($data){
// $salutation = array(1 => 'mr', 2 => 'ms', 3=>null);
if (isset($data['billing_salutation'])) {
$data['billing_salutation'] = (int) $data['billing_salutation'];
//$salutation = array(1 => 'mr', 2 => 'ms', 3=>null);
if(isset($data['billing_salutation'])){
$data['billing_salutation'] = (int) $data['billing_salutation'];
$data['billing_salutation'] = $data['billing_salutation'] == 2 ? 'ms' : 'mr';
}
if (isset($data['shipping_salutation'])) {
$data['shipping_salutation'] = (int) $data['shipping_salutation'];
$data['shipping_salutation'] = $data['shipping_salutation'] == 2 ? 'ms' : 'mr';
if(isset($data['shipping_salutation'])){
$data['shipping_salutation'] = (int) $data['shipping_salutation'];
$data['shipping_salutation'] = $data['shipping_salutation'] == 2 ? 'ms' : 'mr';
}
$ret = [];
$needs = ['billing_salutation', 'billing_company', 'billing_firstname', 'billing_lastname', 'billing_address', 'billing_address_2', 'billing_zipcode', 'billing_city', 'billing_country_id', 'billing_phone', 'billing_email',
$needs = [ 'billing_salutation', 'billing_company', 'billing_firstname', 'billing_lastname', 'billing_address', 'billing_address_2', 'billing_zipcode', 'billing_city', 'billing_country_id', 'billing_phone', 'billing_email',
'shipping_salutation', 'shipping_company', 'shipping_firstname', 'shipping_lastname', 'shipping_address', 'shipping_address_2', 'shipping_zipcode', 'shipping_city', 'shipping_country_id', 'shipping_phone',
'same_as_billing', // 'has_buyed', 'subscribed',
'same_as_billing', //'has_buyed', 'subscribed',
'wp_order_number', 'wp_order_date'];
foreach ($needs as $need) {
foreach ($needs as $need){
$ret[$need] = isset($data[$need]) ? $data[$need] : null;
if ($need === 'billing_country_id') {
@ -570,37 +565,35 @@ class ShoppingUserController extends Controller
$ret['wp_order_date'] = Carbon::parse($ret['wp_order_date'])->toDateTimeString();
}
if ($need === 'same_as_billing') {
$ret['same_as_billing'] = isset($data['same_as_billing']) ? $data['same_as_billing'] : true;
$ret['same_as_billing'] = isset($data['same_as_billing']) ? $data['same_as_billing'] : true;
}
}
$ret['has_buyed'] = true;
$ret['subscribed'] = false;
return $ret;
}
private function prepareOrder($wp_shopping_order, $shopping_user, $wp_invoice_path, $api_notice)
{
private function prepareOrder($wp_shopping_order, $shopping_user, $wp_invoice_path, $api_notice){
Yard::instance('shopping')->destroy();
$ret = [];
if (is_array($wp_shopping_order)) {
if(is_array($wp_shopping_order)){
foreach ($wp_shopping_order as $order) {
// $object = json_decode(json_encode($order), FALSE);
//$object = json_decode(json_encode($order), FALSE);
$order = (object) $order;
$error = [];
if (! isset($order->article) || ! isset($order->qty) || ! isset($order->price)) {
$error[] = 'article parameter is missing';
if (!isset($order->article) || !isset($order->qty) || !isset($order->price)) {
$error[] = "article parameter is missing";
} else {
$product = Product::whereWpNumber($order->article)->first();
if (! $product) {
$error[] = 'article not found';
if (!$product) {
$error[] = "article not found";
} else {
if ($order->price != ($product->price * 100)) {
$error[] = 'different price: '.($product->price * 100);
$error[] = "different price: " . ($product->price * 100);
}
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), (int) $order->qty, $product->price, false, false, ['image' => [], 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
$cartItem = Yard::instance('shopping')->add($product->id, $product->name, (int) $order->qty, $product->price, false, false, ['image' => [], 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission]);
Yard::setTax($cartItem->rowId, $product->getTaxWith());
}
}
@ -609,7 +602,7 @@ class ShoppingUserController extends Controller
}
$ShippingCountry = ShippingCountry::whereCountryId($shopping_user->shipping_country_id)->first();
if ($ShippingCountry) {
if($ShippingCountry){
Yard::instance('shopping')->setShippingCountryWithPrice($ShippingCountry->id);
}
$shopping_order = $this->makeShoppingOrder($shopping_user, $wp_invoice_path, $api_notice);
@ -618,18 +611,15 @@ class ShoppingUserController extends Controller
$shopping_user->shopping_order = $shopping_order;
Yard::instance('shopping')->destroy();
}
return $ret;
}
private function makeShoppingOrder($shopping_user, $wp_invoice_path, $api_notice)
{
private function makeShoppingOrder($shopping_user, $wp_invoice_path, $api_notice){
$data = [
'shopping_user_id' => $shopping_user->id,
'auth_user_id' => $shopping_user->auth_user_id,
'country_id' => Yard::instance('shopping')->getShippingCountryId(),
'language' => \App::getLocale(),
'user_shop_id' => auth()->user()->user_sponsor->shop->id,
'payment_for' => 7,
'member_id' => $shopping_user->member_id,
@ -650,22 +640,23 @@ class ShoppingUserController extends Controller
'mode' => $shopping_user->mode,
];
$shopping_order = $shopping_user->shopping_order;
if ($shopping_order) {
if($shopping_order){
$shopping_order->fill($data);
$shopping_order->save();
} else {
$shopping_order = ShoppingOrder::create($data);
}else{
$shopping_order= ShoppingOrder::create($data);
}
$items = Yard::instance('shopping')->content();
$shopping_order->shopping_order_items()->each(function ($model) use ($items, $shopping_order) {
$shopping_order->shopping_order_items()->each(function($model) use ($items, $shopping_order) {
foreach ($items as $item) {
$price_net = Yard::instance('shopping')->rowPriceNet($item, 2, '.', '');
$tax = $item->price - $price_net;
if ($model->row_id === $item->rowId) {
$model->fill([
'shopping_order_id' => $shopping_order->id,
'row_id' => $item->rowId,
'row_id' => $item->rowId,
'product_id' => $item->id,
'qty' => $item->qty,
'price' => $item->price,
@ -677,21 +668,20 @@ class ShoppingUserController extends Controller
'points' => $item->options->points,
'slug' => $item->options->slug,
])->save();
return false;
}
}
return $model->delete();
});
foreach ($items as $item) {
if (! ShoppingOrderItem::where('shopping_order_id', $shopping_order->id)->where('row_id', $item->rowId)->count()) {
if (!ShoppingOrderItem::where('shopping_order_id', $shopping_order->id)->where('row_id', $item->rowId)->count()){
$price_net = Yard::instance('shopping')->rowPriceNet($item, 2, '.', '');
$tax = $item->price - $price_net;
ShoppingOrderItem::create([
'shopping_order_id' => $shopping_order->id,
'row_id' => $item->rowId,
'row_id' => $item->rowId,
'product_id' => $item->id,
'qty' => $item->qty,
'price' => $item->price,
@ -701,27 +691,27 @@ class ShoppingUserController extends Controller
'price_vk_net' => $shopping_order->getPriceVkNetBy($item->id),
'discount' => $item->options->no_commission ? 0 : $shopping_order->getUserDiscount(),
'points' => $item->options->points,
'slug' => $item->options->slug,
'slug' => $item->options->slug
]);
}
}
$shopping_order->makeTaxSplit();
return $shopping_order;
}
public function orderStatusSendMail(ShoppingOrder $shopping_order)
{
public function orderStatusSendMail(ShoppingOrder $shopping_order){
$bcc = [];
$user_mail = $shopping_order->shopping_user->member->email;
if ($shopping_order->mode === 'dev') {
if($shopping_order->mode === 'dev'){
$bcc[] = config('app.checkout_test_mail');
} else {
}else{
$bcc[] = config('app.checkout_mail');
}
Mail::to($user_mail)->bcc($bcc)->locale($shopping_order->getLocale())->send(new MailCheckout($shopping_order->txaction, $shopping_order, null, false, $shopping_order->mode));
Mail::to($user_mail)->bcc($bcc)->send(new MailCheckout($shopping_order->txaction, $shopping_order, null, false, $shopping_order->mode));
}
}
}

0
app/Http/Controllers/AttributeController.php Normal file → Executable file
View file

0
app/Http/Controllers/Auth/ForgotPasswordController.php Normal file → Executable file
View file

36
app/Http/Controllers/Auth/LoginController.php Normal file → Executable file
View file

@ -6,7 +6,6 @@ use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
@ -41,47 +40,12 @@ class LoginController extends Controller
$this->middleware('guest')->except('logout');
}
public function showLoginForm()
{
//login als Kunde, dann zum Login wechseln
if(Auth::guard('customers')->check()){
return redirect()->route('change_login');
}
return view('auth.login');
}
public function showChangeLogin(){
if(Auth::guard('customers')->check()){
return view('auth.change');
}
if(Auth::guard('user')->check()){
return redirect(route('home'));
}
return redirect(route('login'));
}
public function confirmChangeLogin(Request $request)
{
//$url = Util::getMyMivitaShopUrl();
$user_shop_domain = session('user_shop_domain');
$locale = session('locale');
Auth::guard('customers')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
session(['user_shop_domain' => $user_shop_domain]);
session(['locale' => $locale]);
return redirect()->route('login');
}
protected function authenticated(Request $request, $user)
{
$user->last_login = date('Y-m-d H:i:s');
$user->save();
}
protected function handleUserWasAuthenticated(Request $request, $throttles)
{

0
app/Http/Controllers/Auth/RegisterController.php Normal file → Executable file
View file

0
app/Http/Controllers/Auth/ResetPasswordController.php Normal file → Executable file
View file

View file

@ -32,7 +32,7 @@ class BusinessCommissionController extends Controller
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_months' => HTMLHelper::$months,
'filter_years' => HTMLHelper::getYearRange(),
'filter_members' => $filter_members,
'filter_show' => $this->filter_show,
@ -85,7 +85,7 @@ class BusinessCommissionController extends Controller
if(intval(Request::get('commissions_filter_show')) === 1){
$query->where(function($q) {
return $q->where('user_businesses.commission_pp_total', '>', 0)
return $q->where('user_businesses.commission_team_total', '>', 0)
->orWhere('user_businesses.commission_shop_sales', '>', 0);
});
}
@ -108,20 +108,20 @@ class BusinessCommissionController extends Controller
data-back=""
data-modal="modal-xl"
data-init_from="user"
data-route="'.route('modal_load').'"><span class="fa fa-eye"></span></button>';
data-route="'.route('modal_load').'"><span class="far fa-eye"></span></button>';
})*/
->addColumn('commission_total', function (UserBusiness $UserBusiness) {
$commission_total = $UserBusiness->commission_pp_total + $UserBusiness->commission_shop_sales;
$commission_total = $UserBusiness->commission_team_total + $UserBusiness->commission_shop_sales;
return $commission_total > 0 ?
'<span class="badge badge-outline-info">'.formatNumber($commission_total).' &euro;</span>'
: $commission_total.' &euro;';
})
->addColumn('commission_pp_total', function (UserBusiness $UserBusiness) {
return $UserBusiness->commission_pp_total > 0 ?
'<span class="badge badge-outline-success">'.formatNumber($UserBusiness->commission_pp_total).' &euro;</span>'
: $UserBusiness->commission_pp_total.' &euro;';
->addColumn('commission_team_total', function (UserBusiness $UserBusiness) {
return $UserBusiness->commission_team_total > 0 ?
'<span class="badge badge-outline-success">'.formatNumber($UserBusiness->commission_team_total).' &euro;</span>'
: $UserBusiness->commission_team_total.' &euro;';
})
->addColumn('commission_shop_sales', function (UserBusiness $UserBusiness) {
return $UserBusiness->commission_shop_sales > 0 ?
@ -157,13 +157,13 @@ class BusinessCommissionController extends Controller
})*/
->orderColumn('id', 'id $1')
->orderColumn('commission_pp_total', 'commission_pp_total $1')
->orderColumn('commission_team_total', 'commission_team_total $1')
->orderColumn('commission_shop_sales', 'commission_shop_sales $1')
->orderColumn('email', 'users.email $1')
->orderColumn('m_account', 'm_account $1')
->orderColumn('first_name', 'first_name $1')
->orderColumn('last_name', 'last_name $1')
->rawColumns(['id', 'commission_total', 'commission_pp_total', 'commission_shop_sales', 'active_account'])
->rawColumns(['id', 'commission_total', 'commission_team_total', 'commission_shop_sales', 'active_account'])
->make(true);
}
}

View file

@ -17,7 +17,7 @@ class BusinessController extends Controller
private $filter_active = [1 => 'aktiv', 2 => 'nicht aktiv', 3 => 'alle'];
private $month;
private $year;
public function __construct()
{
$this->middleware('admin');
@ -25,28 +25,27 @@ class BusinessController extends Controller
public function show()
{
abort(403, 'This page is removed');
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_months' => HTMLHelper::$months,
'filter_years' => HTMLHelper::getYearRange(),
'filter_active' => $this->filter_active,
];
return view('admin.business.show', $data);
}
public function structure()
{
//abort(403, 'This page is removed');
$this->setFilterVars();
$this->month = session('business_user_filter_month');
$this->year = session('business_user_filter_year');
$TreeCalcBot = new TreeCalcBot($this->month, $this->year, 'admin');
$TreeCalcBot->initStructureAdmin();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_months' => HTMLHelper::$months,
'filter_years' => HTMLHelper::getYearRange(),
'TreeCalcBot' => $TreeCalcBot,
];
@ -55,16 +54,16 @@ class BusinessController extends Controller
public function userDetail($user_id)
{
abort(403, 'This page is removed');
$user = User::findOrFail($user_id);
$this->setFilterVars();
$data = [];
$data['month'] = session('business_user_filter_month');
$data['year'] = session('business_user_filter_year');
$TreeCalcBot = new TreeCalcBot($data['month'], $data['year'], 'admin');
$TreeCalcBot->initBusinesslUserDetail($user);
if (!$TreeCalcBot->business_user) {
if(!$TreeCalcBot->business_user){
abort(403, 'no user found');
}
return view('admin.business.user_detail', compact('TreeCalcBot', 'user', 'data'));
@ -85,52 +84,43 @@ class BusinessController extends Controller
//return back();
}
private function setFilterVars()
{
private function setFilterVars(){
if (!session('business_user_filter_month')) {
if(!session('business_user_filter_month')){
session(['business_user_filter_month' => intval(date('m'))]);
}
if (!session('business_user_filter_year')) {
if(!session('business_user_filter_year')){
session(['business_user_filter_year' => intval(date('Y'))]);
}
if (!session('business_user_filter_active')) {
if(!session('business_user_filter_active')){
session(['business_user_filter_active' => 1]);
}
if (!session('business_user_filter_depiction')) {
session(['business_user_filter_depiction' => 'active']);
}
if (Request::get('business_user_filter_depiction')) {
session(['business_user_filter_depiction' => Request::get('business_user_filter_depiction')]);
}
if (Request::get('business_user_filter_name')) {
if(Request::get('business_user_filter_name')){
session(['business_user_filter_name' => Request::get('business_user_filter_name')]);
} else {
session(['business_user_filter_name' => '']);
}
if (Request::get('business_user_filter_active')) {
if(Request::get('business_user_filter_active')){
session(['business_user_filter_active' => Request::get('business_user_filter_active')]);
}
if (Request::get('business_user_filter_month')) {
if(Request::get('business_user_filter_month')){
session(['business_user_filter_month' => Request::get('business_user_filter_month')]);
}
if (Request::get('business_user_filter_year')) {
if(Request::get('business_user_filter_year')){
session(['business_user_filter_year' => Request::get('business_user_filter_year')]);
}
}
public function userDatatable()
{
{
$this->month = Request::get('business_user_filter_month');
$this->year = Request::get('business_user_filter_year');
//only the currently month get from Users -> older month from UserBusiness
return $this->userCurrentlyDatatable();
if (TreeCalcBot::isFromStored($this->month, $this->year)) {
//return $this->userCurrentlyDatatable();
if(TreeCalcBot::isFromStored($this->month, $this->year)){
return $this->userStoredDatatable();
} else {
}else{
return $this->userCurrentlyDatatable();
}
}
@ -138,16 +128,16 @@ class BusinessController extends Controller
private function initStoredSearch($archive = false, $request = true)
{
$this->setFilterVars();
$query = UserBusiness::select('user_businesses.*')->where('month', $this->month)->where('year', $this->year);
if (Request::get('business_user_filter_active')) {
if (Request::get('business_user_filter_active') == 1) {
if(Request::get('business_user_filter_active')){
if(Request::get('business_user_filter_active') == 1){
$query->where('user_businesses.active_account', 1);
}
if (Request::get('business_user_filter_active') == 2) {
if(Request::get('business_user_filter_active') == 2){
$query->where('user_businesses.active_account', 0);
}
if (Request::get('business_user_filter_active') == 3) {
if(Request::get('business_user_filter_active') == 3){
//both -> payment_account only not null
}
}
@ -155,18 +145,18 @@ class BusinessController extends Controller
}
private function userStoredDatatable()
{
{
$query = $this->initStoredSearch();
return \DataTables::eloquent($query)
->addColumn('id', function (UserBusiness $userBusiness) {
return '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $userBusiness->user_id . '"
data-id="'.$userBusiness->user_id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>' .
(config('app.debug') === true ? '<a href="' . route('admin_business_user_detail', [$userBusiness->user_id]) . '" class="btn icon-btn btn-xs btn-primary"><span class="fa fa-calculator"></span></a>' : '');
data-route="'.route('modal_load').'"><span class="far fa-calculator"></span></button>';
//.'<a href="' . route('admin_business_user_detail', [$userBusiness->user_id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="far fa-calculator"></span></a>';
})
->addColumn('m_account', function (UserBusiness $userBusiness) {
return $userBusiness->m_account;
@ -175,19 +165,19 @@ class BusinessController extends Controller
return $userBusiness->user_level_name;
})
->addColumn('is_qual_kp', function (UserBusiness $userBusiness) {
if ($userBusiness->m_level_id) {
if($userBusiness->m_level_id){
$isQualKP = ($userBusiness->sales_volume_points_sum >= $userBusiness->qual_kp) ? true : false;
return '<span class="badge ' . ($isQualKP ? 'badge-outline-success' : 'badge-outline-danger') . '"> KU ' . $userBusiness->qual_kp . '</span>';
return '<span class="badge '.($isQualKP ? 'badge-outline-success' : 'badge-outline-danger').'"> KD '.$userBusiness->qual_kp.'</span>';
}
return '-';
})
->addColumn('sales_volume_KP_points', function (UserBusiness $userBusiness) {
return '<div class="no-line-break">' . $userBusiness->sales_volume_points_sum . '</div>' .
'<span class="small no-line-break">E: ' . $userBusiness->sales_volume_KP_points . ' | S: ' . $userBusiness->sales_volume_points_shop . '</span>';
->addColumn('sales_volume_points', function (UserBusiness $userBusiness) {
return '<div class="no-line-break">'.$userBusiness->sales_volume_points_sum.'</div>'.
'<span class="small no-line-break">E: '.$userBusiness->sales_volume_points.' | S: '.$userBusiness->sales_volume_points_shop.'</span>';
})
->addColumn('sales_volume_total', function (UserBusiness $userBusiness) {
return '<div class="no-line-break">' . formatNumber($userBusiness->sales_volume_total_sum) . ' &euro;</div>' .
'<span class="small no-line-break">E: ' . formatNumber($userBusiness->sales_volume_total) . ' | S: ' . formatNumber($userBusiness->sales_volume_total_shop) . '</span>';
return '<div class="no-line-break">'.formatNumber($userBusiness->sales_volume_total_sum).' &euro;</div>'.
'<span class="small no-line-break">E: '.formatNumber($userBusiness->sales_volume_total).' | S: '.formatNumber($userBusiness->sales_volume_total_shop).'</span>';
})
->addColumn('email', function (UserBusiness $userBusiness) {
return $userBusiness->email;
@ -199,23 +189,23 @@ class BusinessController extends Controller
return $userBusiness->last_name;
})
->addColumn('sponsor', function (UserBusiness $userBusiness) {
if ($userBusiness->sponsor) {
if($userBusiness->sponsor){
$sponsor = "";
if ($userBusiness->sponsor->is_sponsor) {
$sponsor .= $userBusiness->sponsor->first_name . " " . $userBusiness->sponsor->last_name;
$sponsor .= " &nbsp;" . '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $userBusiness->sponsor->user_id . '"
if($userBusiness->sponsor->is_sponsor){
$sponsor .= $userBusiness->sponsor->first_name." ".$userBusiness->sponsor->last_name;
$sponsor .= " &nbsp;".'<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$userBusiness->sponsor->user_id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
$sponsor .= '<span class="small no-line-break">' . $userBusiness->sponsor->email;
$sponsor .= ' | ' . $userBusiness->sponsor->m_account;
$sponsor .= '</span>';
}
data-route="'.route('modal_load').'"><span class="far fa-calculator"></span></button><br>';
$sponsor .= '<span class="small no-line-break">'.$userBusiness->sponsor->email;
$sponsor .= ' | '.$userBusiness->sponsor->m_account;
$sponsor .= '</span>';
}
return $sponsor;
}
return '-';
@ -228,25 +218,25 @@ class BusinessController extends Controller
return $userBusiness->active_date ? formatDate($userBusiness->active_date) : "-";
})
->filterColumn('m_account', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("m_account LIKE ?", '%' . $keyword . '%');
->filterColumn('m_account', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("m_account LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("first_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("first_name LIKE ?", '%' . $keyword . '%');
->filterColumn('last_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("last_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('last_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("last_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('email', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("email LIKE ?", '%' . $keyword . '%');
->filterColumn('email', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("email LIKE ?", '%'.$keyword.'%');
}
})
@ -256,32 +246,32 @@ class BusinessController extends Controller
->orderColumn('first_name', 'first_name $1')
->orderColumn('last_name', 'last_name $1')
->orderColumn('active_account', 'payment_account $1')
->rawColumns(['id', 'is_qual_kp', 'sales_volume_KP_points', 'sales_volume_total', 'sponsor', 'active_account'])
->rawColumns(['id', 'is_qual_kp', 'sales_volume_points', 'sales_volume_total', 'sponsor', 'active_account'])
->make(true);
}
private function initCurrentlySearch($archive = false, $request = true)
{
$this->setFilterVars();
$query = User::join('user_accounts', 'account_id', '=', 'user_accounts.id')
->select('users.*', 'user_accounts.m_account', 'user_accounts.first_name', 'user_accounts.last_name')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.m_level', "!=", null)
->where('users.payment_account', "!=", null);
->select('users.*', 'user_accounts.m_account', 'user_accounts.first_name', 'user_accounts.last_name')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.m_level', "!=", null)
->where('users.payment_account', "!=", null);
// $query = User::with('account')->select('users.*')
if (Request::get('business_user_filter_active')) {
if (Request::get('business_user_filter_active') == 1) {
// $query = User::with('account')->select('users.*')
if(Request::get('business_user_filter_active')){
if(Request::get('business_user_filter_active') == 1){
$query->where('users.payment_account', ">=", now());
}
if (Request::get('business_user_filter_active') == 2) {
if(Request::get('business_user_filter_active') == 2){
$query->where('users.payment_account', "<", now());
}
if (Request::get('business_user_filter_active') == 3) {
if(Request::get('business_user_filter_active') == 3){
//both -> payment_account only not null
}
}
@ -289,41 +279,41 @@ class BusinessController extends Controller
}
private function userCurrentlyDatatable()
{
{
$query = $this->initCurrentlySearch();
return \DataTables::eloquent($query)
->addColumn('id', function (User $user) {
return '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $user->id . '"
data-id="'.$user->id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>' .
(config('app.debug') === true ? '<a href="' . route('admin_business_user_detail', [$user->id]) . '" class="btn icon-btn btn-xs btn-primary"><span class="fa fa-calculator"></span></a>' : '');
data-route="'.route('modal_load').'"><span class="far fa-calculator"></span></button>';
//.'<a href="' . route('admin_business_user_detail', [$user->id]) . '" class="btn icon-btn btn-sm btn-info"><span class="far fa-calculator"></span></a>';
})
->addColumn('m_account', function (User $user) {
return $user->account ? $user->account->m_account : '';
})
->addColumn('user_level', function (User $user) {
return $user->user_level ? $user->user_level->getLang('name') : '';
return $user->user_level ? $user->user_level->name : '';
})
->addColumn('is_qual_kp', function (User $user) {
if ($user->user_level) {
if($user->user_level){
$qual_kp = $user->user_level->qual_kp;
$sales_volume_points_sum = $user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_KP_sum');
$sales_volume_points_sum = $user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_sum');
$isQualKP = ($sales_volume_points_sum >= $qual_kp) ? true : false;
return '<span class="badge ' . ($isQualKP ? 'badge-outline-success' : 'badge-outline-warning-dark') . '"> KU ' . $qual_kp . '</span>';
return '<span class="badge '.($isQualKP ? 'badge-outline-success' : 'badge-outline-danger').'"> KD '.$qual_kp.'</span>';
}
return '-';
})
->addColumn('sales_volume_KP_points', function (User $user) {
return '<div class="no-line-break">' . $user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_KP_sum') . '</div>' .
'<span class="small no-line-break">E: ' . $user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_KP_points') . ' | S: ' . $user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_shop') . '</span>';
})
->addColumn('sales_volume_points', function (User $user) {
return '<div class="no-line-break">'.$user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_sum').'</div>'.
'<span class="small no-line-break">E: '.$user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points').' | S: '.$user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_points_shop').'</span>';
})
->addColumn('sales_volume_total', function (User $user) {
return '<div class="no-line-break">' . formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total_sum')) . ' &euro;</div>' .
'<span class="small no-line-break">E: ' . formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total')) . ' | S: ' . formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total_shop')) . '</span>';
return '<div class="no-line-break">'.formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total_sum')).' &euro;</div>'.
'<span class="small no-line-break">E: '.formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total')).' | S: '.formatNumber($user->getUserSalesVolumeBy($this->month, $this->year, 'sales_volume_total_shop')).'</span>';
})
->addColumn('email', function (User $user) {
return $user->email;
@ -335,24 +325,24 @@ class BusinessController extends Controller
return $user->account ? $user->account->last_name : '';
})
->addColumn('sponsor', function (User $user) {
if ($user->user_sponsor) {
if($user->user_sponsor){
$sponsor = "";
if ($user->user_sponsor->account) {
$sponsor .= $user->user_sponsor->account->first_name . " " . $user->user_sponsor->account->last_name;
$sponsor .= " &nbsp;" . '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $user->user_sponsor->id . '"
if($user->user_sponsor->account){
$sponsor .= $user->user_sponsor->account->first_name." ".$user->user_sponsor->account->last_name;
$sponsor .= " &nbsp;".'<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$user->user_sponsor->id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
}
$sponsor .= '<span class="small no-line-break">' . $user->user_sponsor->email;
if ($user->user_sponsor->account) {
$sponsor .= ' | ' . $user->user_sponsor->account->m_account;
}
$sponsor .= '</span>';
data-route="'.route('modal_load').'"><span class="far fa-calculator"></span></button><br>';
}
$sponsor .= '<span class="small no-line-break">'.$user->user_sponsor->email;
if($user->user_sponsor->account){
$sponsor .= ' | '.$user->user_sponsor->account->m_account;
}
$sponsor .= '</span>';
return $sponsor;
}
return '-';
@ -364,25 +354,25 @@ class BusinessController extends Controller
->addColumn('payment_account_date', function (User $user) {
return $user->payment_account ? $user->getPaymentAccountDateFormat(false) : "-";
})
->filterColumn('m_account', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("m_account LIKE ?", '%' . $keyword . '%');
->filterColumn('m_account', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("m_account LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("first_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("first_name LIKE ?", '%' . $keyword . '%');
->filterColumn('last_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("last_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('last_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("last_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('email', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("email LIKE ?", '%' . $keyword . '%');
->filterColumn('email', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("email LIKE ?", '%'.$keyword.'%');
}
})
->orderColumn('id', 'm_account $1')
@ -391,7 +381,7 @@ class BusinessController extends Controller
->orderColumn('email', 'email $1')
->orderColumn('last_name', 'last_name $1')
->orderColumn('active_account', 'payment_account $1')
->rawColumns(['id', 'is_qual_kp', 'sales_volume_KP_points', 'sales_volume_total', 'sponsor', 'active_account'])
->rawColumns(['id', 'is_qual_kp', 'sales_volume_points', 'sales_volume_total', 'sponsor', 'active_account'])
->make(true);
}
}
}
}

View file

@ -1,575 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserBusiness;
use App\Models\UserBusinessStructure;
use App\Services\BusinessPlan\BusinessUserRepository;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\Services\BusinessPlan\TreeHelperOptimized;
use App\Services\BusinessPlan\TreeHtmlRenderer;
use App\Services\HTMLHelper;
use App\Services\NextLevelBadgeHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Request;
/**
* Optimierte Version des BusinessController
*
* Verbesserungen:
* - Nutzt TreeCalcBotOptimized für bessere Performance
* - Optimierte Datenbankabfragen durch Repository Pattern
* - Memory-effiziente Verarbeitung großer Datenmengen
* - Robuste Fehlerbehandlung mit Logging
* - Performance-Monitoring für Debugging
*/
class BusinessControllerOptimized extends Controller
{
private $filter_active = [1 => 'aktiv', 2 => 'nicht aktiv', 3 => 'alle'];
private $filter_next_level = [
0 => 'Alle Status',
1 => 'Qualifiziert (grün)',
2 => 'In Arbeit (gelb)',
3 => 'Kein Level (rot)'
];
private $month;
private $year;
public function __construct()
{
$this->middleware('admin');
}
/**
* Zeigt die Business-Übersicht (identisch zur Original-Version)
*/
public function show()
{
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_years' => HTMLHelper::getYearRange(),
'filter_active' => $this->filter_active,
'filter_levels' => $this->getFilterLevels(),
'filter_next_level' => $this->filter_next_level,
'optimized' => true, // Flag für View um zu zeigen, dass optimierte Version läuft
];
return view('admin.business_optimized.show', $data);
}
/**
* Zeigt die Business-Struktur mit optimierter TreeCalcBot-Version
*/
public function structure()
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
try {
$this->setFilterVars();
$this->month = session('business_user_filter_month');
$this->year = session('business_user_filter_year');
Log::info("BusinessControllerOptimized: Building structure for {$this->month}/{$this->year}");
// Verwende optimierte TreeCalcBot-Version
$TreeCalcBot = new TreeCalcBotOptimized($this->month, $this->year, 'admin');
// Prüfe ob Live-Berechnung für Struktur erzwungen wird
$forceLiveCalculation = Request::get('force_live_calculation', false) ||
Request::get('force_live_structure', false) ||
Request::get('live', false);
if ($forceLiveCalculation) {
Log::info("BusinessControllerOptimized: Force live calculation requested");
$TreeCalcBot->initStructureAdmin(true, $forceLiveCalculation); // check=true, forceLiveCalculation=true
} else {
Log::info("BusinessControllerOptimized: Force live calculation not requested");
$TreeCalcBot->initStructureAdmin(); // Standard: verwende gespeicherte wenn verfügbar
}
$endTime = microtime(true);
$endMemory = memory_get_usage();
$executionTime = round(($endTime - $startTime) * 1000, 2);
$memoryUsed = $this->formatBytes($endMemory - $startMemory);
$calculationType = $forceLiveCalculation ? " (LIVE)" : " (CACHE)";
Log::info("BusinessControllerOptimized: Structure built in {$executionTime}ms, Memory: {$memoryUsed}{$calculationType}");
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_years' => HTMLHelper::getYearRange(),
'TreeCalcBot' => $TreeCalcBot,
'performance' => [
'execution_time' => $executionTime,
'memory_used' => $memoryUsed,
'user_count' => $TreeCalcBot->getTotalUserCount(),
'parentless_count' => $TreeCalcBot->isParentless() ? count($TreeCalcBot->__get('parentless')) : 0,
'calculation_type' => $forceLiveCalculation ? 'Live' : 'Cache'
],
'optimized' => true,
'forceLiveCalculation' => $forceLiveCalculation,
];
return view('admin.business_optimized.structure', $data);
} catch (\Exception $e) {
Log::error("BusinessControllerOptimized: Error in structure: " . $e->getMessage());
return view('admin.business_optimized.error', [
'error' => $e->getMessage(),
'month' => $this->month,
'year' => $this->year
]);
}
}
/**
* Zeigt User-Details mit optimierter Performance
*/
public function userDetail($user_id)
{
$startTime = microtime(true);
try {
$user = User::with(['account', 'user_level', 'user_sponsor.account'])->findOrFail($user_id);
$this->setFilterVars();
$data = [];
$data['month'] = session('business_user_filter_month');
$data['year'] = session('business_user_filter_year');
Log::info("BusinessControllerOptimized: Building user detail for user {$user_id}");
$TreeCalcBot = new TreeCalcBotOptimized($data['month'], $data['year'], 'admin');
// Prüfe ob Live-Berechnung über URL-Parameter erzwungen wird
$forceLiveCalculation = Request::get('force_live_calculation', false) ||
Request::get('force_live', false) ||
Request::get('live', false);
if ($forceLiveCalculation) {
Log::info("BusinessControllerOptimized: Force live calculation requested for user {$user_id}");
}
$TreeCalcBot->initBusinesslUserDetail($user, $forceLiveCalculation);
if (!$TreeCalcBot->__get('business_user')) {
Log::warning("BusinessControllerOptimized: No business user found for {$user_id}");
abort(403, 'No business user found');
}
$endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
$data['performance'] = [
'execution_time' => $executionTime,
'user_id' => $user_id,
'calculation_type' => $forceLiveCalculation ? 'Live' : 'Cache'
];
$data['forceLiveCalculation'] = $forceLiveCalculation;
$calculationType = $forceLiveCalculation ? " (LIVE)" : " (CACHE)";
Log::info("BusinessControllerOptimized: User detail built in {$executionTime}ms{$calculationType}");
return view('admin.business_optimized.user_detail', compact('TreeCalcBot', 'user', 'data'));
} catch (\Exception $e) {
Log::error("BusinessControllerOptimized: Error in userDetail for {$user_id}: " . $e->getMessage());
return view('admin.business_optimized.error', [
'error' => $e->getMessage(),
'user_id' => $user_id
]);
}
}
/**
* Store-Funktion (identisch zur Original-Version)
*/
public function userStore($user_id)
{
dd('function on: App\Console\Commands\BusinessStore');
}
/**
* Optimierte DataTable für Users mit besserer Performance
*/
public function userDatatable(): JsonResponse
{
try {
$this->month = Request::get('business_user_filter_month');
$this->year = Request::get('business_user_filter_year');
Log::info("BusinessControllerOptimized: Building datatable for {$this->month}/{$this->year}");
// Prüfe ob optimierte Repository-Daten verfügbar sind
if (TreeCalcBotOptimized::isFromStored($this->month, $this->year)) {
return $this->userStoredDatatableOptimized();
} else {
return $this->userCurrentlyDatatableOptimized();
}
} catch (\Exception $e) {
Log::error("BusinessControllerOptimized: Error in userDatatable: " . $e->getMessage());
return response()->json([
'error' => 'Datatable could not be loaded: ' . $e->getMessage()
], 500);
}
}
/**
* Optimierte Stored-Datatable mit besserer Query-Performance
*/
private function userStoredDatatableOptimized(): JsonResponse
{
$query = $this->initStoredSearchOptimized();
return \DataTables::eloquent($query)
->addColumn('id', function (UserBusiness $userBusiness) {
return TreeHelperOptimized::generateActionButtons($userBusiness->user_id);
})
->addColumn('m_account', function (UserBusiness $userBusiness) {
return e($userBusiness->m_account);
})
->addColumn('user_level', function (UserBusiness $userBusiness) {
return e($userBusiness->user_level_name);
})
->addColumn('is_qual_kp', function (UserBusiness $userBusiness) {
return TreeHelperOptimized::generateQualKPBadge($userBusiness);
})
->addColumn('sales_volume_KP_points', function (UserBusiness $userBusiness) {
return TreeHelperOptimized::generateSalesVolumeDisplay($userBusiness, 'points');
})
->addColumn('sales_volume_total', function (UserBusiness $userBusiness) {
return TreeHelperOptimized::generateSalesVolumeDisplay($userBusiness, 'total');
})
->addColumn('email', function (UserBusiness $userBusiness) {
return e($userBusiness->email);
})
->addColumn('first_name', function (UserBusiness $userBusiness) {
return e($userBusiness->first_name);
})
->addColumn('last_name', function (UserBusiness $userBusiness) {
return e($userBusiness->last_name);
})
->addColumn('sponsor', function (UserBusiness $userBusiness) {
return TreeHelperOptimized::generateSponsorDisplay($userBusiness);
})
->addColumn('active_account', function (UserBusiness $userBusiness) {
return get_active_badge($userBusiness->active_account);
})
->addColumn('next_level_qualified', function (UserBusiness $userBusiness) {
return NextLevelBadgeHelper::generateBadgeFromUserBusiness($userBusiness);
})
->addColumn('payment_account_date', function (UserBusiness $userBusiness) {
return $userBusiness->active_date ? formatDate($userBusiness->active_date) : "-";
})
->filterColumn('m_account', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_businesses.m_account LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('first_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_businesses.first_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('last_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_businesses.last_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('email', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_businesses.email LIKE ?", '%' . $keyword . '%');
}
})
->orderColumn('id', 'id $1')
->orderColumn('m_account', 'm_account $1')
->orderColumn('email', 'email $1')
->orderColumn('first_name', 'first_name $1')
->orderColumn('last_name', 'last_name $1')
->orderColumn('active_account', 'payment_account $1')
->rawColumns(['id', 'is_qual_kp', 'sales_volume_KP_points', 'sales_volume_total', 'sponsor', 'active_account', 'next_level_qualified'])
->make(true);
}
/**
* Optimierte Currently-Datatable mit Repository Pattern
*/
private function userCurrentlyDatatableOptimized(): JsonResponse
{
$repository = new BusinessUserRepository($this->month, $this->year);
// Nutze Repository für optimierte Abfragen
$query = $this->initCurrentlySearchOptimized();
return \DataTables::eloquent($query)
->addColumn('id', function (User $user) {
return TreeHelperOptimized::generateActionButtons($user->id);
})
->addColumn('m_account', function (User $user) {
return $user->account ? e($user->account->m_account) : '';
})
->addColumn('user_level', function (User $user) {
return $user->user_level ? e($user->user_level->getLang('name')) : '';
})
->addColumn('is_qual_kp', function (User $user) {
return TreeHelperOptimized::generateQualKPBadgeForUser($user, $this->month, $this->year);
})
->addColumn('sales_volume_KP_points', function (User $user) {
return TreeHelperOptimized::generateSalesVolumeDisplayForUser($user, 'points', $this->month, $this->year);
})
->addColumn('sales_volume_total', function (User $user) {
return TreeHelperOptimized::generateSalesVolumeDisplayForUser($user, 'total', $this->month, $this->year);
})
->addColumn('email', function (User $user) {
return e($user->email);
})
->addColumn('first_name', function (User $user) {
return $user->account ? e($user->account->first_name) : '';
})
->addColumn('last_name', function (User $user) {
return $user->account ? e($user->account->last_name) : '';
})
->addColumn('sponsor', function (User $user) {
return TreeHelperOptimized::generateSponsorDisplayForUser($user);
})
->addColumn('active_account', function (User $user) {
return get_active_badge($user->isActiveAccount());
})
->addColumn('next_level_qualified', function (User $user) {
// Für Live-DataTable: Verwende bereits berechnete Daten wenn verfügbar
$userBusiness = UserBusiness::where('user_id', $user->id)
->where('month', $this->month)
->where('year', $this->year)
->first();
if ($userBusiness) {
return NextLevelBadgeHelper::generateBadgeFromUserBusiness($userBusiness);
}
return NextLevelBadgeHelper::renderNoDataBadge();
})
->addColumn('payment_account_date', function (User $user) {
return $user->payment_account ? $user->getPaymentAccountDateFormat(false) : "-";
})
->filterColumn('m_account', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_accounts.m_account LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('first_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_accounts.first_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('last_name', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("user_accounts.last_name LIKE ?", '%' . $keyword . '%');
}
})
->filterColumn('email', function ($query, $keyword) {
if ($keyword != "") {
$query->whereRaw("users.email LIKE ?", '%' . $keyword . '%');
}
})
->orderColumn('id', 'users.id $1')
->orderColumn('m_account', 'user_accounts.m_account $1')
->orderColumn('first_name', 'user_accounts.first_name $1')
->orderColumn('last_name', 'user_accounts.last_name $1')
->orderColumn('email', 'users.email $1')
->orderColumn('active_account', 'users.payment_account $1')
->rawColumns(['id', 'is_qual_kp', 'sales_volume_KP_points', 'sales_volume_total', 'sponsor', 'active_account', 'next_level_qualified'])
->make(true);
}
// ===== PRIVATE HELPER METHODS =====
/**
* Optimierte Stored Search Query
*/
private function initStoredSearchOptimized()
{
$this->setFilterVars();
$query = UserBusiness::select('user_businesses.*')
->where('month', $this->month)
->where('year', $this->year);
$activeFilter = Request::get('business_user_filter_active') ?: session('business_user_filter_active');
if ($activeFilter == 1) {
$query->where('user_businesses.active_account', 1);
} elseif ($activeFilter == 2) {
$query->where('user_businesses.active_account', 0);
}
// activeFilter == 3 bedeutet alle (keine weitere Einschränkung)
$levelFilter = Request::get('business_user_filter_level') ?: session('business_user_filter_level');
if ($levelFilter && $levelFilter != 0) {
$query->where('user_businesses.m_level_id', $levelFilter);
}
$nextLevelFilter = Request::get('business_user_filter_next_level') ?: session('business_user_filter_next_level');
if ($nextLevelFilter && $nextLevelFilter != 0) {
switch ($nextLevelFilter) {
case 1: // Qualifiziert (grün) - hat next_qual_user_level
$query->whereNotNull('user_businesses.next_qual_user_level')
->where('user_businesses.next_qual_user_level', '!=', '[]');
break;
case 2: // In Arbeit (gelb) - hat next_can_user_level aber kein next_qual_user_level
$query->where(function ($q) {
$q->whereNull('user_businesses.next_qual_user_level')
->orWhere('user_businesses.next_qual_user_level', '=', '[]');
})
->whereNotNull('user_businesses.next_can_user_level')
->where('user_businesses.next_can_user_level', '!=', '[]');
break;
case 3: // Kein Level (rot) - hat weder next_qual noch next_can
$query->where(function ($q) {
$q->where(function ($q1) {
$q1->whereNull('user_businesses.next_qual_user_level')
->orWhere('user_businesses.next_qual_user_level', '=', '[]');
})
->where(function ($q2) {
$q2->whereNull('user_businesses.next_can_user_level')
->orWhere('user_businesses.next_can_user_level', '=', '[]');
});
});
break;
}
}
return $query;
}
/**
* Optimierte Currently Search Query mit besseren Joins
*/
private function initCurrentlySearchOptimized()
{
$this->setFilterVars();
$query = User::with(['account', 'user_level', 'user_sponsor.account'])
->select('users.*', 'user_accounts.m_account', 'user_accounts.first_name', 'user_accounts.last_name')
->leftJoin('user_accounts', 'users.id', '=', 'user_accounts.id')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.m_level', '!=', null)
->where('users.payment_account', '!=', null);
$activeFilter = Request::get('business_user_filter_active') ?: session('business_user_filter_active');
if ($activeFilter == 1) {
$query->where('users.payment_account', '>=', now());
} elseif ($activeFilter == 2) {
$query->where('users.payment_account', '<', now());
}
// activeFilter == 3 bedeutet alle (keine weitere Einschränkung)
$levelFilter = Request::get('business_user_filter_level') ?: session('business_user_filter_level');
if ($levelFilter && $levelFilter != 0) {
$query->where('users.m_level', $levelFilter);
}
// Next-Level-Filter wird bei Live-Berechnungen ignoriert (Performance-Gründe)
// Dieser Filter funktioniert nur mit gespeicherten Daten
$nextLevelFilter = Request::get('business_user_filter_next_level') ?: session('business_user_filter_next_level');
if ($nextLevelFilter && $nextLevelFilter != 0) {
Log::info("BusinessControllerOptimized: Next-Level-Filter bei Live-Berechnung ignoriert (Performance-Gründe)");
}
return $query;
}
/**
* Filter-Variablen setzen (identisch zur Original-Version)
*/
private function setFilterVars()
{
if (!session('business_user_filter_month')) {
session(['business_user_filter_month' => intval(date('m'))]);
}
if (!session('business_user_filter_year')) {
session(['business_user_filter_year' => intval(date('Y'))]);
}
if (!session('business_user_filter_active')) {
session(['business_user_filter_active' => 1]);
}
if (!session('business_user_filter_level')) {
session(['business_user_filter_level' => 0]);
}
if (!session('business_user_filter_next_level')) {
session(['business_user_filter_next_level' => 0]);
}
if (!session('business_user_filter_depiction')) {
session(['business_user_filter_depiction' => 'active']);
}
if (Request::get('business_user_filter_depiction')) {
session(['business_user_filter_depiction' => Request::get('business_user_filter_depiction')]);
}
if (Request::get('business_user_filter_name')) {
session(['business_user_filter_name' => Request::get('business_user_filter_name')]);
} else {
session(['business_user_filter_name' => '']);
}
if (Request::get('business_user_filter_active')) {
session(['business_user_filter_active' => Request::get('business_user_filter_active')]);
}
if (Request::get('business_user_filter_level')) {
session(['business_user_filter_level' => Request::get('business_user_filter_level')]);
} else {
session(['business_user_filter_level' => 0]);
}
if (Request::get('business_user_filter_next_level')) {
session(['business_user_filter_next_level' => Request::get('business_user_filter_next_level')]);
} else {
session(['business_user_filter_next_level' => 0]);
}
if (Request::get('business_user_filter_month')) {
session(['business_user_filter_month' => Request::get('business_user_filter_month')]);
}
if (Request::get('business_user_filter_year')) {
session(['business_user_filter_year' => Request::get('business_user_filter_year')]);
}
}
/**
* Formatiert Bytes in lesbare Einheiten
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = array('B', 'KB', 'MB', 'GB', 'TB');
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
/**
* Holt verfügbare User Level für Filter
*/
private function getFilterLevels(): array
{
$levels = [0 => 'Alle Level'];
$userLevels = \App\Models\UserLevel::orderBy('pos')->get(['id', 'name']);
foreach ($userLevels as $level) {
$levels[$level->id] = $level->name;
}
return $levels;
}
// Performance-optimierte Badge-Generierung wurde in NextLevelBadgeHelper ausgelagert
// Alte performance-lastige Methoden wurden entfernt um die Datatable-Performance zu verbessern
}

View file

@ -1,14 +1,18 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers;
use Carbon;
use Request;
use App\Services\Payment;
use App\Models\UserInvoice;
use App\Services\HTMLHelper;
use App\Models\UserSalesVolume;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\HTMLHelper;
use Request;
class BusinessPointsController extends Controller
{
public function __construct()
{
$this->middleware('admin');
@ -16,100 +20,67 @@ class BusinessPointsController extends Controller
public function index()
{
$filter_members = UserSalesVolume::join('users', 'user_id', '=', 'users.id')
->groupBy('user_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')
->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get();
->groupBy('user_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')
->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get();
$this->setFilterVars();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_months' => HTMLHelper::$months,
'filter_years' => HTMLHelper::getYearRange(),
'filter_members' => $filter_members,
'filter_status_types' => UserSalesVolume::getTransStatusType(),
];
return view('admin.business.points', $data);
}
public function store()
{
public function store(){
$data = Request::all();
if (! isset($data['action'])) {
if(!isset($data['action'])){
return back();
}
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
if(!isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')){
\Session()->flash('alert-error', 'Das Passwort ist falsch.');
return back();
return back();
}
if (! isset($data['is_checked_action'])) {
if(!isset($data['is_checked_action'])){
\Session()->flash('alert-error', 'Änderung nicht bestätigt');
return back();
return back();
}
if ($data['action'] === 'add_user_sales_volume') {
if($data['action'] === 'add_user_sales_volume'){
SalesPointsVolume::addSalesPointsVolume($data);
return back(); }
return back();
}
if ($data['action'] === 'edit_user_sales_volume') {
if($data['action'] === 'edit_user_sales_volume'){
SalesPointsVolume::editSalesPointsVolume($data);
return back();
return back();
}
dd($data);
return redirect(route('admin_business_points'));
}
public function recalculate()
{
$user_id = Request::get('points_filter_member_id');
$month = Request::get('points_filter_month');
$year = Request::get('points_filter_year');
if (! $user_id) {
\Session()->flash('alert-error', 'Kein Berater ausgewählt.');
private function setFilterVars(){
return back();
}
if (! $month || ! $year) {
\Session()->flash('alert-error', 'Monat und Jahr müssen angegeben sein.');
return back();
}
try {
SalesPointsVolume::reCalculateSalesPointsVolume($user_id, $month, $year);
\Session()->flash('alert-success', 'Punkte für den ausgewählten Berater im Monat '.str_pad($month, 2, '0', STR_PAD_LEFT).'/'.$year.' wurden erfolgreich neu berechnet.');
} catch (\Exception $e) {
\Session()->flash('alert-error', 'Fehler bei der Neuberechnung: '.$e->getMessage());
}
return back();
}
private function setFilterVars()
{
if (! session('points_filter_month')) {
if(!session('points_filter_month')){
session(['points_filter_month' => intval(date('m'))]);
}
if (! session('points_filter_year')) {
if(!session('points_filter_year')){
session(['points_filter_year' => intval(date('Y'))]);
}
session(['points_filter_member_id' => Request::get('points_filter_member_id')]);
session(['points_filter_status_type_id' => Request::get('points_filter_status_type_id')]);
if (Request::get('points_filter_month')) {
if(Request::get('points_filter_month')){
session(['points_filter_month' => Request::get('points_filter_month')]);
}
if (Request::get('points_filter_year')) {
if(Request::get('points_filter_year')){
session(['points_filter_year' => Request::get('points_filter_year')]);
}
}
@ -118,69 +89,21 @@ class BusinessPointsController extends Controller
{
$this->setFilterVars();
// $query = UserSalesVolume::with('user', 'user.account')->with('shopping_order')->select('user_sales_volumes.*')
//$query = UserSalesVolume::with('user', 'user.account')->with('shopping_order')->select('user_sales_volumes.*')
$query = UserSalesVolume::join('users', 'user_id', '=', 'users.id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')
->select('user_sales_volumes.*', 'users.email', 'user_accounts.m_account', 'user_accounts.first_name', 'user_accounts.last_name')
->where('user_sales_volumes.month', '=', Request::get('points_filter_month'))
->where('user_sales_volumes.year', '=', Request::get('points_filter_year'));
->select('user_sales_volumes.*', 'users.email', 'user_accounts.m_account', 'user_accounts.first_name', 'user_accounts.last_name')
->where('user_sales_volumes.month', '=', Request::get('points_filter_month'))
->where('user_sales_volumes.year', '=', Request::get('points_filter_year'));
if (Request::get('points_filter_member_id')) {
if(Request::get('points_filter_member_id')){
$query->where('user_sales_volumes.user_id', '=', Request::get('points_filter_member_id'));
}
if (Request::get('points_filter_status_type_id')) {
$query->where('user_sales_volumes.status', '=', Request::get('points_filter_status_type_id'));
}
return $query;
}
public function getSummary()
{
$user_id = Request::get('points_filter_member_id');
$month = Request::get('points_filter_month');
$year = Request::get('points_filter_year');
if (! $user_id || ! $month || ! $year) {
return response()->json([
'success' => false,
'data' => null,
]);
}
// Hole den letzten Eintrag für den User im Monat, da dort die akkumulierten Summen stehen
$lastEntry = UserSalesVolume::where('user_id', $user_id)
->where('month', $month)
->where('year', $year)
->orderBy('id', 'DESC')
->first();
if (! $lastEntry) {
return response()->json([
'success' => false,
'data' => null,
]);
}
return response()->json([
'success' => true,
'data' => [
'month_KP_points' => $lastEntry->month_KP_points ?? 0,
'month_TP_points' => $lastEntry->month_TP_points ?? 0,
'month_shop_points' => $lastEntry->month_shop_points ?? 0,
'month_total_net' => $lastEntry->month_total_net ?? 0,
'month_shop_total_net' => $lastEntry->month_shop_total_net ?? 0,
'total_KP_points' => ($lastEntry->month_KP_points ?? 0) + ($lastEntry->month_shop_points ?? 0),
'total_TP_points' => ($lastEntry->month_TP_points ?? 0) + ($lastEntry->month_shop_points ?? 0),
'total_net' => ($lastEntry->month_total_net ?? 0) + ($lastEntry->month_shop_total_net ?? 0),
],
]);
}
public function datatable()
{
public function datatable(){
$query = $this->initSearch();
return \DataTables::eloquent($query)
->addColumn('id', function (UserSalesVolume $UserSalesVolume) {
return '<button type="button" class="btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
@ -189,74 +112,53 @@ class BusinessPointsController extends Controller
data-back=""
data-modal="modal-xl"
data-init_from="user"
data-route="'.route('modal_load').'"><span class="fa fa-eye"></span></button>';
data-route="'.route('modal_load').'"><span class="far fa-eye"></span></button>';
})
->addColumn('order', function (UserSalesVolume $UserSalesVolume) {
if ($UserSalesVolume->shopping_order) {
// Status 1 = Beraterbestellung
if ($UserSalesVolume->status === 1) {
return '<a href="'.route('admin_sales_users_detail', [$UserSalesVolume->shopping_order->id]).'" class="btn btn-xs btn-primary">'.$UserSalesVolume->shopping_order->id.'</a>';
if($UserSalesVolume->shopping_order){
if($UserSalesVolume->status === 1){
return '<a href="' . route('admin_sales_users_detail', [$UserSalesVolume->shopping_order->id]) . '" class="btn btn-xs btn-primary">'.$UserSalesVolume->shopping_order->id.'</a>';
}
// Status 2/3 = Shop-Bestellung
if ($UserSalesVolume->status === 2 || $UserSalesVolume->status === 3) {
return '<a href="'.route('admin_sales_customers_detail', [$UserSalesVolume->shopping_order->id]).'" class="btn btn-xs btn-secondary">'.$UserSalesVolume->shopping_order->id.'</a>';
}
// Status 6 = Storno - Link zur ursprünglichen Bestellung mit Storno-Icon
if ($UserSalesVolume->status === 6) {
// Prüfen ob Berater- oder Shop-Bestellung anhand des payment_for Feldes
$route = ($UserSalesVolume->shopping_order->payment_for === 6 || $UserSalesVolume->shopping_order->payment_for === 7)
? route('admin_sales_customers_detail', [$UserSalesVolume->shopping_order->id])
: route('admin_sales_users_detail', [$UserSalesVolume->shopping_order->id]);
return '<a href="'.$route.'" class="btn btn-xs btn-danger" title="Storno-Eintrag"><i class="fa fa-undo"></i> '.$UserSalesVolume->shopping_order->id.'</a>';
if($UserSalesVolume->status === 2 || $UserSalesVolume->status === 3){
return '<a href="' . route('admin_sales_customers_detail', [$UserSalesVolume->shopping_order->id]) . '" class="btn btn-xs btn-secondary">'.$UserSalesVolume->shopping_order->id.'</a>';
}
}
return '';
})
->addColumn('points', function (UserSalesVolume $UserSalesVolume) {
return formatNumber($UserSalesVolume->points);
})
->addColumn('total_net', function (UserSalesVolume $UserSalesVolume) {
return formatNumber($UserSalesVolume->total_net).' &euro;';
})
->addColumn('status_turnover', function (UserSalesVolume $UserSalesVolume) {
return '<span class="badge badge-pill badge-'.$UserSalesVolume->getStatusTurnoverColor().'">'.$UserSalesVolume->getStatusTurnoverType().'</span>';
})
->addColumn('status', function (UserSalesVolume $UserSalesVolume) {
return '<span class="badge badge-pill badge-'.$UserSalesVolume->getStatusColor().'">'.$UserSalesVolume->getStatusType().'</span>';
})
->addColumn('status_points', function (UserSalesVolume $UserSalesVolume) {
return '<span class="badge badge-pill badge-'.$UserSalesVolume->getStatusPointsColor().'">'.$UserSalesVolume->getStatusPointsType().'</span>';
})
->addColumn('message', function (UserSalesVolume $UserSalesVolume) {
return '<span class="no-line-break">'.$UserSalesVolume->message.'</span>';
})
->addColumn('info', function (UserSalesVolume $UserSalesVolume) {
return '<span class="no-line-break">'.$UserSalesVolume->info.'</span>';
})
->filterColumn('m_account', function ($query, $keyword) {
if ($keyword != '') {
$query->whereRaw('m_account LIKE ?', '%'.$keyword.'%');
->filterColumn('m_account', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("m_account LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("first_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('first_name', function ($query, $keyword) {
if ($keyword != '') {
$query->whereRaw('first_name LIKE ?', '%'.$keyword.'%');
->filterColumn('last_name', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("last_name LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('last_name', function ($query, $keyword) {
if ($keyword != '') {
$query->whereRaw('last_name LIKE ?', '%'.$keyword.'%');
->filterColumn('email', function($query, $keyword) {
if($keyword != ""){
$query->whereRaw("email LIKE ?", '%'.$keyword.'%');
}
})
->filterColumn('email', function ($query, $keyword) {
if ($keyword != '') {
$query->whereRaw('email LIKE ?', '%'.$keyword.'%');
}
})
->orderColumn('id', 'id $1')
->orderColumn('order', 'order $1')
->orderColumn('status', 'status $1')
@ -267,7 +169,7 @@ class BusinessPointsController extends Controller
->orderColumn('m_account', 'm_account $1')
->orderColumn('first_name', 'first_name $1')
->orderColumn('last_name', 'last_name $1')
->rawColumns(['id', 'order', 'status_turnover', 'status', 'status_points', 'message', 'info', 'total_net'])
->rawColumns(['id', 'order', 'status', 'message', 'info', 'total_net'])
->make(true);
}
}
}

95
app/Http/Controllers/CategoryController.php Normal file → Executable file
View file

@ -28,10 +28,10 @@ class CategoryController extends Controller
public function edit($id)
{
if ($id == "new") {
if($id == "new"){
$model = new Category();
$model->active = true;
} else {
}else{
$model = Category::findOrFail($id);
}
$data = [
@ -46,9 +46,9 @@ class CategoryController extends Controller
{
$data = Request::all();
if ($data['action'] === 'save-product_category') {
if($data['action'] === 'save-product_category'){
if ($data['id'] === 'new') {
if($data['id'] === 'new'){
$ProductCategory = ProductCategory::create([
'pos' => $data['pos'],
'product_id' => $data['product_id'],
@ -56,9 +56,9 @@ class CategoryController extends Controller
]);
\Session()->flash('alert-save', '1');
return redirect(route('admin_product_category_edit', [$ProductCategory->category_id]));
} else {
}else{
$ProductCategory = ProductCategory::findOrFail($data['id']);
if ($ProductCategory->category_id != $data['category_id']) {
if($ProductCategory->category_id != $data['category_id']){
abort(404);
}
$ProductCategory->pos = $data['pos'];
@ -67,64 +67,64 @@ class CategoryController extends Controller
\Session()->flash('alert-save', '1');
return redirect(route('admin_product_category_edit', [$ProductCategory->category_id]));
}
}
if ($data['action'] === 'save-form') {
if($data['action'] === 'save-form'){
$data['active'] = isset($data['active']) ? true : false;
$data['parent_id'] = isset($data['parent_id']) ? $data['parent_id'] : null;
if ($data['id'] == "new") {
if($data['id'] == "new"){
$model = Category::create($data);
} else {
}else{
$model = Category::find($data['id']);
$model->fill($data)->save();
}
$trans = [];
if (!empty($data['trans_name'])) {
foreach ($data['trans_name'] as $lang => $value) {
if ($value && $value != null) {
if(!empty($data['trans_name'])){
foreach ($data['trans_name'] as $lang => $value){
if($value && $value != null){
$trans[$lang] = $value;
}
}
}
$model->trans_name = $trans;
$model->save();
$trans = [];
if (!empty($data['trans_headline'])) {
foreach ($data['trans_headline'] as $lang => $value) {
if ($value && $value != null) {
if(!empty($data['trans_headline'])){
foreach ($data['trans_headline'] as $lang => $value){
if($value && $value != null){
$trans[$lang] = $value;
}
}
}
$model->trans_headline = $trans;
$model->save();
\Session()->flash('alert-save', '1');
return redirect(route('admin_product_categories'));
}
}
public function delete($do, $id)
{
public function delete($do, $id){
if ($do === 'product_category') {
if($do === 'product_category'){
$model = ProductCategory::findOrFail($id);
$category = $model->category;
$model->delete();
\Session()->flash('alert-success', 'Eintrag gelöscht');
return redirect(route('admin_product_category_edit', [$category->id]));
}
if ($do === 'category') {
if (ProductCategory::where('category_id', $id)->count()) {
if($do === 'category'){
if(ProductCategory::where('category_id', $id)->count()){
\Session()->flash('alert-error', 'Eintrag hat noch Produkte, erst löschen');
return redirect(route('admin_product_categories'));
}
if (Category::where('parent_id', $id)->count()) {
\Session()->flash('alert-error', 'Eintrag wird als Haupt-Kategorie verwendet');
if(Category::where('parent_id', $id)->count()){
\Session()->flash('alert-error', 'Eintrag wird als Haup-Kategorie verwendet');
return redirect(route('admin_product_categories'));
}
$model = Category::findOrFail($id);
@ -132,12 +132,12 @@ class CategoryController extends Controller
\Session()->flash('alert-success', 'Eintrag gelöscht');
return redirect(route('admin_product_categories'));
}
}
// Upload FILE -----------------------------------------------------------------------------------------------------------------------
public function imageUpload()
{
public function imageUpload(){
$category_id = Request::get('category_id');
$category = Category::findOrFail($category_id);
@ -145,11 +145,12 @@ class CategoryController extends Controller
try {
$image = \App\Services\Slim::getImages('images')[0];
if (isset($image['output']['data'])) {
if ( isset($image['output']['data']) )
{
// Base64 of the image
$data = $image['output']['data'];
$file_ex = array('image/jpeg' => 'jpg', 'image/png' => 'png');
$file_ex = array( 'image/jpeg' => 'jpg', 'image/png' => 'png');
if (!isset($file_ex[$image['output']['type']])) {
\Session()->flash('alert-danger', 'File is not jpg or png!');
@ -165,10 +166,10 @@ class CategoryController extends Controller
$image_name = "";
do {
$image_name = uniqid('', false) . '_' . $name;
} while (\Storage::disk('public')->exists($path . $image_name));
} while (\Storage::disk('public')->exists($path.$image_name));
$data = \Storage::disk('public')->put(
$path . $image_name,
$path.$image_name,
$data
);
@ -183,40 +184,42 @@ class CategoryController extends Controller
$category->headline_image_id = $iq_image->id;
$category->save();
\Session()->flash('alert-success', __('msg.file_uploaded'));
\Session()->flash('alert-success', "Datei hochgeladen");
return redirect(route('admin_product_category_edit', [$category->id]));
}
\Session()->flash('alert-danger', __('msg.file_empty'));
\Session()->flash('alert-danger', "Datei leer");
return redirect(route('admin_product_category_edit', [$category->id]));
} catch (Exception $e) {
\Session()->flash('alert-danger', "Error: " . $e);
}
catch (Exception $e) {
\Session()->flash('alert-danger', "Fehler".$e);
return redirect(route('admin_product_category_edit', [$category->id]));
}
}
public function imageDelete($image_id, $category_id)
{
public function imageDelete($image_id, $category_id){
$category = Category::findOrFail($category_id);
$iq_image = IqImage::findOrFail($image_id);
if ($iq_image->id == $category->iq_image->id) {
$file = 'images/iq_images/' . $iq_image->filename;
if($iq_image->id == $category->iq_image->id){
$file = 'images/iq_images/'.$iq_image->filename;
\Storage::disk('public')->delete($file);
$category->headline_image_id = NULL;
$category->save();
$iq_image->delete();
\Session()->flash('alert-success', __('msg.file_deleted'));
\Session()->flash('alert-success', "Datei gelöscht");
return redirect(route('admin_product_category_edit', [$category->id]));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
\Session()->flash('alert-danger', "Datei nicht gefunden");
return redirect(route('admin_product_category_edit', [$category->id]));
}
public function imageAttribute($image_id, $attr, $val = false)
{
public function imageAttribute($image_id, $attr, $val = false){
$iq_image = IqImage::findOrFail($image_id);
@ -225,5 +228,7 @@ class CategoryController extends Controller
\Session()->flash('alert-success', "Wert gespeichert");
return redirect()->back();
}
}
}

0
app/Http/Controllers/Controller.php Normal file → Executable file
View file

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