23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:35:23 +01:00
parent a67740d6ef
commit 7325edebbd
245 changed files with 29211 additions and 4796 deletions

65
.gitignore vendored
View file

@ -1,16 +1,42 @@
# General
# 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
.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
@ -18,30 +44,15 @@ Icon
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.idea/
.claude/
.cursor/
.cursorrules/
.cursorrules.md/
.cursorrules.md.txt/
.cursorrules.md.txt.txt/
.cursorrules.md.txt.txt.txt/
Icon
# Project specific
_static/
_work/
_storage/
/vendor
/node_modules
/storage/language
/storage/framework
/storage/logs
/public/vendor
/storage/app

266
CLAUDE.md
View file

@ -1,187 +1,161 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Projekt-Übersicht
## Project Overview
**mivita.care** - Laravel 11 CRM für Netzwerk-Marketing (MLM) mit E-Commerce, Provisionsberechnung, DHL-Versand und Multi-Tenancy.
This is a Laravel 11 business e-commerce application called "mivita.care" that handles multi-level marketing structures, product sales, payments, and DHL shipping integration. The application supports multi-tenancy through domain resolution and includes comprehensive business analytics and commission calculations.
## 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 |
## 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
### Core Laravel Commands
### Laravel Basis
```bash
# Run development server
php artisan serve
# Run scheduled tasks
php artisan schedule:run
# Clear application cache
php artisan cache:clear
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
# Generate IDE helper files
php artisan ide-helper:generate
php artisan ide-helper:models
php artisan ide-helper:meta
# Database migrations
php artisan migrate
php artisan migrate:rollback
```
### Testing
```bash
# Run tests using Pest
./vendor/bin/pest
# Run specific test
./vendor/bin/pest --filter=TestName
```
### Code Quality
```bash
# Format code using Laravel Pint
./vendor/bin/pint
composer run format
./vendor/bin/pint # Code formatieren
./vendor/bin/pest # Tests ausführen
./vendor/bin/pest --filter=TestName # Einzelner Test
```
### Asset Compilation
### IDE Helper
```bash
# Install node dependencies
npm install
# Development build
npm run dev
npm run watch
# Production build
npm run production
# Asset postinstall (rebuilds node-sass)
npm run postinstall
php artisan ide-helper:generate
php artisan ide-helper:models
php artisan ide-helper:meta
```
### Business-Specific Commands
### Assets
```bash
# Business structure calculations (optimized version - recommended)
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
# Legacy business structure command
php artisan business:store {month} {year}
# Daten löschen
php artisan business:clear-data {month} {year} [--force]
# Clear business data
php artisan business:clear-data {month} {year}
php artisan business:clear-data {month} {year} --force
# Level Reports
php artisan business:level-reports {month} {year}
# User management
# User Management
php artisan user:cleanup
php artisan user:make_abo_order
# Payment processing
# Zahlungen
php artisan payments:check-accounts
# Level reports (new feature)
php artisan business:level-reports {month} {year}
# Test account management
# Test Account
php artisan business:test-account
```
## Architecture Overview
## Scheduled Tasks (Cron)
### Multi-Level Marketing Structure
- **TreeCalcBot/TreeCalcBotOptimized**: Core business logic for calculating commission structures and multi-level hierarchies
- **BusinessPlan Services**: Handle commission calculations, sales volumes, and business structure management
- **BusinessController/BusinessControllerOptimized**: Handle business administration and analytics
| 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 |
### Domain Architecture
- **Multi-tenant setup** using domain resolution
- **DomainService**: Manages domain-specific configurations and routing
- **DomainResolver Middleware**: Routes requests based on domain
## Docker Environment
### Key Service Classes
- **Payment Services**: Handle various payment methods (Payone, credit systems, invoices)
- **DHL Integration**: Complete shipping solution with label generation and tracking
- **Shopping Cart Systems**: Multiple cart implementations (AboOrderCart, HomepartyCart, ShopApiOrderCart)
- **User Management**: Comprehensive user hierarchy and team management
### Database Structure
- Business users with hierarchical relationships
- Product catalog with categories and ingredients
- Order and payment tracking
- Commission and sales volume calculations
- Multi-domain configurations
### Custom Packages
- **Gloudemans\Shoppingcart**: Custom shopping cart implementation
- **Acme\Dhl**: DHL shipping integration package
- **Alban\LaravelCollectiveSpatieHtmlParser**: HTML parsing utilities
## Scheduled Tasks (Cron Jobs)
The application runs several daily scheduled tasks (defined in `app/Console/Kernel.php`):
- `02:00` - Payment account checks (`payments:check-accounts`)
- `03:00` - Business structure optimization (`store-optimized 0 0`)
- `03:30` - User cleanup (`user:cleanup`)
- `04:00` - Automated subscription orders (`user:make_abo_order`)
## Important File Locations
- **Business Commands**: `app/Console/Commands/Business*.php`
- **Service Classes**: `app/Services/` (extensive service layer)
- **Controllers**: `app/Http/Controllers/` (40+ controllers)
- **Custom Packages**: `packages/` directory
- **Dev Documentation**: `dev/code/Services/*.md` (contains optimization guides)
## Development Notes
### Business Optimization Features
- Use `TreeCalcBotOptimized` for all new business calculation features
- Memory monitoring and automatic garbage collection built into optimized commands
- Comprehensive error handling with graceful degradation
- Performance logging and monitoring capabilities
### DHL Shipping Integration
- Full DHL API integration with both Developer API and Business Customer API support
- Automatic label generation and tracking
- Sandbox/production mode switching
- Complete shipping workflow integration
### Multi-Domain Support
- Each domain can have different configurations
- Domain-specific routing and middleware
- Subdomain management through console commands
### Asset Management
- Laravel Mix for asset compilation
- Appwork theme integration with Bootstrap 4
- Vendor assets management with SASS compilation
- Custom node-sass rebuild process for compatibility
## Docker Development Environment
The project uses Laravel Sail with Docker Compose:
```bash
# Start services
docker-compose up -d
# Execute artisan commands in container
./vendor/bin/sail artisan [command]
# Access container shell
./vendor/bin/sail shell
docker-compose up -d # Services starten
./vendor/bin/sail artisan [command] # Artisan im Container
./vendor/bin/sail shell # Shell im Container
```
### Services:
- **laravel.test**: Main application (Traefik-enabled with SSL)
- **horizon**: Laravel Horizon queue worker
- **mysql**: MySQL 8.0 database server
- **redis**: Redis cache/session store
- **mailpit**: Email testing interface (accessible at mivita-mail.test)
### Services
- **laravel.test** - Hauptanwendung (Traefik + SSL)
- **horizon** - Queue Worker
- **mysql** - MySQL 8.0
- **redis** - Cache/Session
- **mailpit** - Mail Testing (`mivita-mail.test`)
### Domain Configuration:
- Main domain: `mivita.test`
- Wildcard subdomain support: `*.mivita.test`
- Mail interface: `mivita-mail.test`
- Uses Traefik reverse proxy with SSL
### 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

View file

@ -35,6 +35,22 @@ class BusinessStoreOptimized extends Command
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;
}
/**
* Create a new command instance.
*
@ -65,7 +81,7 @@ class BusinessStoreOptimized extends Command
if ($executeDay !== $presentDay) {
$this->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
\Log::channel('cron')->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
return 0;
// return 0;
}
$this->timeStart = microtime(true);
@ -95,9 +111,13 @@ class BusinessStoreOptimized extends Command
$this->userBusinessCommissionsToCredit();
});
$this->executeWithErrorHandling('User Level Update', function () {
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized User Level Update');
$this->userLevelUpdate();
});
// Auskommentierte Prozesse bleiben inaktiv
// $this->userCreatePaymentCreditsPDF();
// $this->userLevelUpdate();
// $this->storeBusinessStructureUsersDetailPeriod(1, 6);
$this->logExecutionTime('COMMAND COMPLETED SUCCESSFULLY');
@ -187,7 +207,11 @@ class BusinessStoreOptimized extends Command
}
}
private function userLevelUpdate()
/**
* 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);
@ -195,21 +219,46 @@ class BusinessStoreOptimized extends Command
$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) {
$this->info('updateLevel: ' . $userBusiness->user->id . ' | ' . $userBusiness->user->email . ' | ' .
'from: ' . $userBusiness->m_level_id . ' ' . $userBusiness->user_level_name . ' | ' .
'to: ' . $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("Updated {$updatedCount} user levels total");
$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;
}
}

View file

@ -2,10 +2,11 @@
namespace App\Console\Commands;
use App\User;
use App\Cron\UserPaymentCredits;
use App\Models\UserBusiness;
use App\Services\BusinessPlan\BusinessUserItemOptimized;
use App\Cron\UserPaymentCredits;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\User;
use Illuminate\Console\Command;
use stdClass;
@ -74,8 +75,10 @@ class BusinessTestAccount extends Command
$date->end_date = date('Y-m-t 23:59:59', strtotime("{$year}-{$month}-01"));
// Teste BusinessUserItemOptimized
$businessUserItem = new BusinessUserItemOptimized($date);
$businessUserItem->makeUserFromModel($user, true);
$TreeCalcBot = new TreeCalcBotOptimized($date->month, $date->year, 'admin');
$TreeCalcBot->initBusinesslUserDetail($user, true);
$businessUserItem = $TreeCalcBot->getItem();
$bUser = $businessUserItem->getBUser();

View file

@ -0,0 +1,430 @@
<?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

@ -0,0 +1,97 @@
<?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

@ -0,0 +1,226 @@
<?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=14 : 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)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aktualisiert Tracking-Status für alle aktiven DHL Sendungen und sendet automatisch E-Mails bei Transit-Status';
/**
* 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');
$this->info('DHL Tracking Update gestartet');
$this->info("Optionen: --days={$days}, --send-emails=" . ($sendEmails ? 'ja' : 'nein') . ', --dry-run=' . ($dryRun ? 'ja' : 'nein'));
if ($testEmail) {
$this->info("Test-Modus: E-Mails werden an {$testEmail} gesendet");
}
if ($orderId) {
$this->info("Filter: Nur Order-ID {$orderId}");
}
$this->newLine();
// Hole alle aktiven Sendungen der letzten X Tage
$query = DhlShipment::active()
->where('created_at', '>=', now()->subDays($days))
->whereNotNull('dhl_shipment_no');
// Filter nach Order-ID wenn angegeben
if ($orderId) {
$query->where('order_id', $orderId);
}
$shipments = $query->orderBy('created_at', 'desc')->get();
$total = $shipments->count();
$this->info("Gefundene aktive Sendungen: {$total}");
if ($total === 0) {
$this->info('Keine Sendungen zum Aktualisieren gefunden.');
return self::SUCCESS;
}
$bar = $this->output->createProgressBar($total);
$bar->start();
$trackingService = new DhlTrackingService;
$stats = [
'updated' => 0,
'failed' => 0,
'emails_sent' => 0,
'skipped' => 0,
];
foreach ($shipments as $shipment) {
try {
$oldStatus = $shipment->status;
if (! $dryRun) {
// Tracking aktualisieren
$result = $trackingService->updateTracking($shipment, ['auto_retrack' => false]);
if ($result['success']) {
$shipment->refresh();
$stats['updated']++;
// Prüfen ob E-Mail gesendet werden soll
if ($sendEmails && $this->shouldSendEmail($shipment, $oldStatus)) {
$this->sendTrackingEmail($shipment, $testEmail);
$stats['emails_sent']++;
}
} else {
$stats['failed']++;
Log::warning('[DHL Cron] Tracking update failed', [
'shipment_id' => $shipment->id,
'message' => $result['message'] ?? 'Unknown error',
]);
}
} else {
$stats['skipped']++;
}
} catch (\Exception $e) {
$stats['failed']++;
Log::error('[DHL Cron] Exception during tracking update', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
$bar->advance();
}
$bar->finish();
$this->newLine(2);
// Zusammenfassung
$this->info('Zusammenfassung:');
$this->table(
['Metrik', 'Anzahl'],
[
['Gesamt', $total],
['Aktualisiert', $stats['updated']],
['Fehlgeschlagen', $stats['failed']],
['E-Mails gesendet', $stats['emails_sent']],
['Übersprungen (Dry-Run)', $stats['skipped']],
]
);
Log::info('[DHL Cron] Tracking update completed', $stats);
return self::SUCCESS;
}
/**
* 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

@ -0,0 +1,106 @@
<?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

@ -0,0 +1,319 @@
<?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

@ -0,0 +1,321 @@
<?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

@ -0,0 +1,380 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use App\Models\UserAbo;
use App\Cron\UserMakeOrder;
use App\Services\AboHelper;
use App\Models\UserAboOrder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
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,
]);
});
} 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

@ -12,6 +12,7 @@ use App\Services\AboHelper;
use App\Models\UserAboOrder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class UserMakeAboOrder extends Command
{
@ -87,12 +88,17 @@ class UserMakeAboOrder extends Command
\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");
\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) {
@ -104,7 +110,38 @@ class UserMakeAboOrder extends Command
$this->info("Verarbeite Abo: {$userAbo->id} (PayoneUserid: {$userAbo->payone_userid})");
try {
$shoppingOrder = $this->makeOrder($userAbo);
// 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', [
@ -163,6 +200,11 @@ class UserMakeAboOrder extends Command
$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,
@ -170,6 +212,10 @@ class UserMakeAboOrder extends Command
'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;
}
@ -180,6 +226,7 @@ class UserMakeAboOrder extends Command
'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', [
@ -196,9 +243,12 @@ class UserMakeAboOrder extends Command
$response
);
$this->updateAbo($userAbo, $shoppingOrder, 3);
// 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',
@ -207,13 +257,25 @@ class UserMakeAboOrder extends Command
];
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 (\Exception $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
@ -222,13 +284,19 @@ class UserMakeAboOrder extends Command
'trace' => $e->getTraceAsString()
]);
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
// Bei Exception: Bestellung speichern falls vorhanden, aber Abo nicht aktualisieren
if ($shoppingOrder) {
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
}
}
return $shoppingOrder;
}
/**
* Aktualisiert das Abo nach einer Bestellung
* Aktualisiert das Abo nach einer erfolgreichen Bestellung
* Aktualisiert next_date für den nächsten Abo-Zyklus
*
* @param UserAbo $userAbo
* @param mixed $shoppingOrder
@ -237,7 +305,7 @@ class UserMakeAboOrder extends Command
*/
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
{
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo', [
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo nach erfolgreicher Zahlung', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'status' => $status
@ -245,6 +313,8 @@ class UserMakeAboOrder extends Command
$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(),
@ -254,25 +324,87 @@ class UserMakeAboOrder extends Command
$updateData['status'] = $status;
}
try {
$userAbo->update($updateData);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => $status,
'paid' => false,
]);
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo erfolgreich aktualisiert', [
'abo_id' => $userAbo->id,
'next_date' => $updateData['next_date']
]);
});
} 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
}
}

View file

@ -3,9 +3,11 @@
namespace App\Console;
use App\Console\Commands\BusinessStore;
use App\Console\Commands\BusinessStoreOptimized;
use App\Console\Commands\CheckPaymentsAccount;
use App\Console\Commands\UserMakeAboOrder;
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;
@ -18,15 +20,16 @@ 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)
@ -36,10 +39,19 @@ class Kernel extends ConsoleKernel
// 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('store-optimized 0 0')->dailyAt('03:00');
$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');
// Cleanup old log files weekly (keeps logs for 30 days)
$schedule->command('logs:cleanup --days=30')->weekly()->sundays()->at('05:00');
// DHL Tracking Update: Täglich um 06:00 Uhr, automatische E-Mails bei Transit-Status
$schedule->command('dhl:update-tracking --days=14 --send-emails')
->dailyAt('06:00')
->withoutOverlapping()
->runInBackground();
}
/**

View file

@ -1,55 +1,137 @@
<?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');
}
public function getUserBusinessByMonthYear(){
/**
* 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
->where('user_businesses.month', '=', $this->month)
->where('user_businesses.year', '=', $this->year)
->where('user_businesses.next_qual_user_level', '!=', NULL)
->whereNotNull('user_businesses.next_qual_user_level')
->whereRaw("JSON_LENGTH(user_businesses.next_qual_user_level) > 0")
->get();
}
public function makeUserLevelUpdate(UserBusiness $userBusiness, $send_update_mail){
/**
* 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)
{
$ret = false;
$nextQualUserLevel = $userBusiness->next_qual_user_level;
if(!isset($nextQualUserLevel['hasUpdated']) && $userBusiness->user){
$userBusiness->user->m_level = $nextQualUserLevel['id'];
$userBusiness->user->save();
$nextQualUserLevel['hasUpdated'] = 1;
$userBusiness->next_qual_user_level = $nextQualUserLevel;
$userBusiness->save();
$ret = $nextQualUserLevel['id'].' '.$nextQualUserLevel['name'];
if($send_update_mail){
self::sendUpdateMail($userBusiness->user, $userBusiness->total_qual_pp, $nextQualUserLevel['name']);
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;
}
private function sendUpdateMail(User $user, $tp, $to){
// 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'];
$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
}
}
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)
{
$bcc = [];
$email = $user->email;
if (!$email) {
@ -63,6 +145,10 @@ class UserLevelUpdate
} 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));
}
}

View file

@ -1,15 +1,10 @@
<?php
namespace App\Cron;
use Yard;
use App\User;
use stdClass;
use App\Services\Shop;
use App\Models\Product;
use App\Models\UserAbo;
use App\Models\ShoppingOrder;
use App\Services\UserService;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrderItem;
use App\Http\Controllers\Pay\PayoneController;
use App\Services\AboOrderCart;
@ -61,7 +56,7 @@ class UserMakeOrder
return $ret;
}
public function makePayment()
public function makePayment($testmode = false)
{
Log::info('Starte Zahlungsvorgang für UserAbo ID: ' . $this->userAbo->id);
@ -71,7 +66,9 @@ class UserMakeOrder
$amount = $this->shopping_order->subtotal_ws * 100;
$this->pay->setAboPayment($this->userAbo, $amount, 'EUR');
$this->pay->setPersonalData();
$response = $this->pay->ResponseData(true);
$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;
@ -127,12 +124,41 @@ class UserMakeOrder
return false;
}
AboOrderCart::initYard($this->userAbo, $this->shopping_user);
// 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()
]);
if (!$this->userAbo->shopping_user || !$this->userAbo->shopping_user->shopping_order || !$this->userAbo->shopping_user->shopping_order->user_shop) {
Log::error('Fehlende Beziehungsdaten für Bestellerstellung, UserAbo ID: ' . $this->userAbo->id);
return false;
@ -155,7 +181,7 @@ class UserMakeOrder
'points' => $yard->points(),
'weight' => $yard->weight(),
'is_abo' => 1,
'abo_interval' => 0,
'abo_interval' => $this->userAbo->abo_interval ?? 0,
'txaction' => 'prev',
'mode' => $this->userAbo->shopping_user->shopping_order->mode,
]);

View file

@ -57,10 +57,11 @@ class AdminUserController extends Controller
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function store(Request $request)
public function store()
{
$data = Request::all();
$user = User::findOrFail($data['id']);
$user = User::withTrashed()->findOrFail($data['id']);
/* if(isset($data['user-delete'])){
if(isset($data['realy_delete_user'])){
@ -117,6 +118,36 @@ class AdminUserController extends Controller
->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'])) {
$old = $user->getPaymentAccountDateFormat(true);
if (!isset($data['payment_account']) || $data['payment_account'] == "") {
@ -249,6 +280,12 @@ class AdminUserController extends Controller
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>';

View file

@ -20,14 +20,12 @@ class PayoneController extends Controller
{
public function __construct()
public function __construct() {}
public function paymentStatus()
{
}
public function paymentStatus(){
$data = \Request::all();
// test para
@ -124,7 +122,8 @@ class PayoneController extends Controller
'payone',
'error',
'Error:2007 App\Http\Controllers\Api\PayoneController::paymentStatus same txaction - was already paid',
$data
$data,
false
);
//was already paid
print("TSOK");
@ -134,7 +133,8 @@ class PayoneController extends Controller
'payone',
'error',
'Error:2007 App\Http\Controllers\Api\PayoneController::paymentStatus same txaction - show',
$data
$data,
false
);
}
}
@ -183,5 +183,4 @@ class PayoneController extends Controller
print("TSOK");
exit;
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use Carbon;
use Request;
use App\Services\Payment;
@ -38,7 +39,8 @@ class BusinessPointsController extends Controller
return view('admin.business.points', $data);
}
public function store(){
public function store()
{
$data = Request::all();
if (!isset($data['action'])) {
return back();
@ -54,21 +56,47 @@ class BusinessPointsController extends Controller
if ($data['action'] === 'add_user_sales_volume') {
SalesPointsVolume::addSalesPointsVolume($data);
return back(); }
return back();
}
if ($data['action'] === 'edit_user_sales_volume') {
SalesPointsVolume::editSalesPointsVolume($data);
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');
private function setFilterVars(){
if (!$user_id) {
\Session()->flash('alert-error', 'Kein Berater ausgewählt.');
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')) {
session(['points_filter_month' => intval(date('m'))]);
@ -107,7 +135,50 @@ class BusinessPointsController extends Controller
return $query;
}
public function datatable(){
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()
{
$query = $this->initSearch();
return \DataTables::eloquent($query)
@ -131,6 +202,9 @@ class BusinessPointsController extends Controller
}
return '';
})
->addColumn('points', function (UserSalesVolume $UserSalesVolume) {
return formatNumber($UserSalesVolume->points);
})
->addColumn('total_net', function (UserSalesVolume $UserSalesVolume) {
return formatNumber($UserSalesVolume->total_net) . ' &euro;';
})

View file

@ -67,7 +67,6 @@ class CategoryController extends Controller
\Session()->flash('alert-save', '1');
return redirect(route('admin_product_category_edit', [$ProductCategory->category_id]));
}
}
if ($data['action'] === 'save-form') {
@ -109,7 +108,8 @@ class CategoryController extends Controller
}
public function delete($do, $id){
public function delete($do, $id)
{
if ($do === 'product_category') {
$model = ProductCategory::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,8 +145,7 @@ 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'];
@ -189,15 +188,14 @@ class CategoryController extends Controller
}
\Session()->flash('alert-danger', __('msg.file_empty'));
return redirect(route('admin_product_category_edit', [$category->id]));
}
catch (Exception $e) {
} catch (Exception $e) {
\Session()->flash('alert-danger', "Error: " . $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);
@ -212,14 +210,13 @@ class CategoryController extends Controller
\Session()->flash('alert-success', __('msg.file_deleted'));
return redirect(route('admin_product_category_edit', [$category->id]));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
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);
@ -228,7 +225,5 @@ class CategoryController extends Controller
\Session()->flash('alert-success', "Wert gespeichert");
return redirect()->back();
}
}

View file

@ -18,7 +18,6 @@ class CustomerController extends Controller
{
$this->middleware('admin');
$this->customerRepository = $customerRepository;
}
public function index()
@ -47,6 +46,19 @@ class CustomerController extends Controller
return view('admin.customer.detail', $data);
}
public function delete($id)
{
$shopping_user = ShoppingUser::findOrFail($id);
$result = $this->customerRepository->deleteCustomer($shopping_user);
if (!$result) {
return back()->with('alert-error', 'Kunde hat Bestellungen. Löschen nicht möglich.');
} else {
\Session()->flash('alert-success', 'Kunde wurde gelöscht');
return redirect(route('admin_customers'));
}
return redirect(route('admin_customers'));
}
public function edit($id)
{
if ($id === "new") {
@ -59,7 +71,6 @@ class CustomerController extends Controller
'shopping_user' => $shopping_user,
'isAdmin' => true,
'isView' => 'customer',
];
return view('admin.customer.edit', $data);
}
@ -134,7 +145,6 @@ class CustomerController extends Controller
\Session()->flash('alert-save', true);
}
return redirect(route('admin_customer_detail', [$shopping_user->id]));
}
public function getCustomers()

View file

@ -2,12 +2,11 @@
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\CancelShipmentJob;
use App\Jobs\CreateReturnLabelJob;
use App\Jobs\TrackShipmentJob;
// Old DHL model replaced with new package model
use Acme\Dhl\Models\DhlShipment;
use App\Jobs\CancelShipmentJob;
// Old DHL model replaced with new package model
use App\Jobs\CreateReturnLabelJob;
use App\Mail\MailDhlTracking;
use App\Models\ShoppingOrder;
use App\Services\DhlModalService;
use App\Services\DhlShipmentService;
@ -16,18 +15,15 @@ use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
// Import new DHL package and SettingController
use Yajra\DataTables\Facades\DataTables;
use ZipArchive;
// Import new DHL package and SettingController
use Acme\Dhl\DhlManager;
/**
* DHL Shipment Controller
*
@ -54,7 +50,7 @@ class DhlShipmentController extends Controller
{
try {
// Get DHL configuration with admin settings
$settingController = new \App\Http\Controllers\SettingController();
$settingController = new \App\Http\Controllers\SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Create DhlClient with merged configuration
@ -74,34 +70,31 @@ class DhlShipmentController extends Controller
'message' => 'DHL API Verbindung erfolgreich getestet!',
'details' => [
'base_url' => $dhlConfig['base_url'],
'using_admin_config' => !empty($dhlConfig['api_key'])
]
'using_admin_config' => ! empty($dhlConfig['api_key']),
],
];
} else {
$result = [
'success' => false,
'message' => 'DHL API Verbindung fehlgeschlagen. Prüfen Sie Ihre Zugangsdaten.'
'message' => 'DHL API Verbindung fehlgeschlagen. Prüfen Sie Ihre Zugangsdaten.',
];
}
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Test login failed', [
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'DHL API Test fehlgeschlagen: ' . $e->getMessage()
'message' => 'DHL API Test fehlgeschlagen: ' . $e->getMessage(),
], 500);
}
}
/**
* Display the DHL Cockpit (main overview)
*
* @param Request $request
* @return View
*/
public function index(Request $request): View
{
@ -118,9 +111,6 @@ class DhlShipmentController extends Controller
/**
* Provides data for the DHL Cockpit DataTable.
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function datatable(Request $request): JsonResponse
{
@ -161,19 +151,22 @@ class DhlShipmentController extends Controller
return '<label class="custom-control custom-checkbox mb-0"><input type="checkbox" class="custom-control-input shipment-checkbox" value="' . $shipment->id . '"><span class="custom-control-label"></span></label>';
})
->editColumn('id', function ($shipment) {
return '<a href="' . route('admin.dhl.show', $shipment) . '" class="text-primary font-weight-semibold">#' . $shipment->id . '</a>';
$class = $shipment->type === 'return' ? 'text-warning font-weight-bold' : 'text-primary font-weight-semibold';
$icon = $shipment->type === 'return' ? '<i class="fas fa-undo mr-1"></i>' : '';
return '<a href="' . route('admin.dhl.show', $shipment) . '" class="' . $class . '">' . $icon . '#' . $shipment->id . '</a>';
})
->addColumn('type', function ($shipment) {
if ($shipment->type == 'outbound') {
return '<span class="badge badge-primary"><i class="fas fa-arrow-right"></i> Ausgehend</span>';
} else {
return '<span class="badge badge-info"><i class="fas fa-undo"></i> Retoure</span>';
return '<span class="badge badge-warning" style="font-size: 0.9rem; font-weight: 600;"><i class="fas fa-undo"></i> RETOURE</span>';
}
})
->addColumn('order', function ($shipment) {
if ($shipment->order_id) {
return '<a href="' . route('admin_sales_customers_detail', $shipment->order_id) . '" class="text-primary">#' . $shipment->order_id . '</a>';
}
return '<span class="text-muted">N/A</span>';
})
->addColumn('customer', function ($shipment) {
@ -192,6 +185,7 @@ class DhlShipmentController extends Controller
'failed' => ['class' => 'danger', 'text' => 'Fehler'],
];
$statusInfo = $statusMap[$shipment->status] ?? ['class' => 'light', 'text' => e($shipment->status)];
return '<span class="badge badge-' . $statusInfo['class'] . '">' . $statusInfo['text'] . '</span>';
})
->addColumn('tracking_status', function ($shipment) {
@ -199,6 +193,7 @@ class DhlShipmentController extends Controller
return '<small class="text-muted">' . e($shipment->tracking_status) . '</small>' .
($shipment->last_tracked_at ? '<br><small class="text-muted">' . $shipment->last_tracked_at->format('d.m.Y H:i') . '</small>' : '');
}
return '<span class="text-muted">-</span>';
})
->editColumn('weight_kg', function ($shipment) {
@ -213,17 +208,29 @@ class DhlShipmentController extends Controller
if ($shipment->label_path) {
$buttons .= '<a href="' . route('admin.dhl.download-label', $shipment) . '" class="btn btn-sm btn-outline-success" data-toggle="tooltip" title="Label herunterladen"><i class="fas fa-download"></i></a>';
}
/* Todo: Add tracking button
if ($shipment->canCancel()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-warning cancel-shipment-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="Sendung stornieren"><i class="fas fa-ban"></i></button>';
// Email button
if ($shipment->dhl_shipment_no && $shipment->canSendTrackingEmail()) {
$emailTitle = $shipment->wasTrackingEmailSent()
? 'Tracking-E-Mail erneut senden (gesendet: ' . $shipment->tracking_email_sent_at->format('d.m.Y H:i') . ')'
: 'Tracking-E-Mail senden';
$emailClass = $shipment->wasTrackingEmailSent() ? 'btn-success' : 'btn-outline-info';
$buttons .= '<button type="button" class="btn btn-sm ' . $emailClass . ' send-tracking-email-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="' . $emailTitle . '"><i class="fas fa-envelope"></i></button>';
}
// Cancel button
if ($shipment->canCancel()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-danger cancel-shipment-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="Sendung stornieren"><i class="fas fa-ban"></i></button>';
}
// Return label button
if ($shipment->type == 'outbound' && ! $shipment->returns()->count()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-info create-return-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="Retourenlabel erstellen"><i class="fas fa-undo"></i></button>';
}
*/
$buttons .= '</div>';
return $buttons;
})
->addColumn('DT_RowClass', function ($shipment) {
return $shipment->type === 'return' ? 'return-shipment' : '';
})
->rawColumns(['checkbox', 'id', 'type', 'order', 'customer', 'dhl_shipment_no', 'status', 'tracking_status', 'actions'])
->make(true);
}
@ -231,7 +238,6 @@ class DhlShipmentController extends Controller
/**
* Show the form for creating a new shipment
*
* @param ShoppingOrder $order
* @return View
*/
public function create(ShoppingOrder $order): View|\Illuminate\Http\RedirectResponse
@ -251,21 +257,18 @@ class DhlShipmentController extends Controller
/**
* Store a new shipment (async via queue)
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request): JsonResponse
{
try {
// Use DhlModalService for validation
$dhlModalService = new DhlModalService();
$dhlModalService = new DhlModalService;
$validationResult = $dhlModalService->validateShipmentData($request->all());
if (! $validationResult['valid']) {
return response()->json([
'success' => false,
'message' => 'Validierungsfehler: ' . implode(', ', $validationResult['errors'])
'message' => 'Validierungsfehler: ' . implode(', ', $validationResult['errors']),
], 422);
}
@ -286,6 +289,8 @@ class DhlShipmentController extends Controller
'shipping_city' => 'required|string|max:50',
'shipping_country_id' => 'required|exists:countries,id',
'shipping_phone' => 'nullable|string|max:20',
'shipping_email' => 'required|email|max:100',
'shipping_postnumber' => 'nullable|string|max:20',
]);
$order = ShoppingOrder::findOrFail($request->order_id);
@ -313,11 +318,11 @@ class DhlShipmentController extends Controller
'auto_track' => $request->get('auto_track', true),
'shipping_address' => $shippingAddress,
'services' => $request->get('services', []),
'dimensions' => $request->only(['length', 'width', 'height'])
'dimensions' => $request->only(['length', 'width', 'height']),
];
// Use DhlShipmentService (handles queue/sync automatically based on config)
$dhlShipmentService = new DhlShipmentService();
$dhlShipmentService = new DhlShipmentService;
$result = $dhlShipmentService->createShipment($order, (float) $request->weight, $options);
Log::info('[DHL Controller] Shipment creation processed', [
@ -336,16 +341,13 @@ class DhlShipmentController extends Controller
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen der Sendung: ' . $e->getMessage()
'message' => 'Fehler beim Erstellen der Sendung: ' . $e->getMessage(),
], 500);
}
}
/**
* Display the specified shipment
*
* @param DhlShipment $shipment
* @return View
*/
public function show(DhlShipment $shipment): View
{
@ -356,10 +358,6 @@ class DhlShipmentController extends Controller
/**
* Cancel the specified shipment
*
* @param Request $request
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function cancel(Request $request, DhlShipment $shipment): JsonResponse
{
@ -368,45 +366,41 @@ class DhlShipmentController extends Controller
if (! $shipment->canCancel()) {
return response()->json([
'success' => false,
'message' => 'Diese Sendung kann nicht mehr storniert werden.'
'message' => 'Diese Sendung kann nicht mehr storniert werden.',
], 422);
}
// Dispatch cancellation job
// Use DhlShipmentService (handles queue/sync automatically based on config)
$options = [
'priority' => $request->get('priority', 'normal')
'priority' => $request->get('priority', 'normal'),
];
CancelShipmentJob::dispatch($shipment, $options);
$dhlShipmentService = new DhlShipmentService;
$result = $dhlShipmentService->cancelShipment($shipment, $options);
Log::info('[DHL Controller] Shipment cancellation job dispatched', [
Log::info('[DHL Controller] Shipment cancellation processed', [
'shipment_id' => $shipment->id,
'shipment_number' => $shipment->shipment_number,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'queued' => $result['queued'] ?? false,
'success' => $result['success'] ?? false,
]);
return response()->json([
'success' => true,
'message' => 'Sendung wird storniert...'
]);
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch shipment cancellation', [
Log::error('[DHL Controller] Failed to process shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: ' . $e->getMessage()
'message' => 'Fehler beim Stornieren der Sendung: ' . $e->getMessage(),
], 500);
}
}
/**
* Create return label for the specified shipment
*
* @param Request $request
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
{
@ -415,7 +409,7 @@ class DhlShipmentController extends Controller
if ($shipment->type !== 'outbound') {
return response()->json([
'success' => false,
'message' => 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.'
'message' => 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.',
], 422);
}
@ -427,45 +421,143 @@ class DhlShipmentController extends Controller
if ($existingReturn) {
return response()->json([
'success' => false,
'message' => 'Für diese Sendung existiert bereits ein Retourenlabel.'
'message' => 'Für diese Sendung existiert bereits ein Retourenlabel.',
], 422);
}
// Check DHL_USE_QUEUE configuration
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
// Dispatch return label creation job
$options = [
'auto_track' => $request->get('auto_track', false),
'priority' => $request->get('priority', 'normal')
'priority' => $request->get('priority', 'normal'),
];
CreateReturnLabelJob::dispatch($shipment, $options);
Log::info('[DHL Controller] Return label creation job dispatched', [
'original_shipment_id' => $shipment->id,
'shipment_number' => $shipment->shipment_number,
'shipment_number' => $shipment->dhl_shipment_no,
]);
return response()->json([
'success' => true,
'message' => 'Retourenlabel wird erstellt...'
'message' => 'Retourenlabel wird im Hintergrund erstellt. Dies kann einige Sekunden dauern.',
]);
} else {
// Create synchronously
$result = $this->createReturnLabelSync($shipment);
return response()->json($result);
}
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch return label creation', [
Log::error('[DHL Controller] Failed to create return label', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen des Retourenlabels: ' . $e->getMessage()
'message' => 'Fehler beim Erstellen des Retourenlabels: ' . $e->getMessage(),
], 500);
}
}
/**
* Create return label synchronously
*/
private function createReturnLabelSync(DhlShipment $shipment): array
{
try {
Log::info('[DHL Controller] Creating return label synchronously', [
'original_shipment_id' => $shipment->id,
]);
// Get DHL configuration
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
// Initialize DHL client
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Use ReturnsService instead of ShippingService
$returnsService = new \Acme\Dhl\Services\ReturnsService($dhlClient);
// Prepare return label data
$order = $shipment->shoppingOrder;
$recipient = $shipment->recipient ?? [];
$returnData = [
'order_id' => $order->id,
'original_shipment_id' => $shipment->id,
'weight_kg' => $shipment->weight_kg,
'label_format' => $shipment->label_format ?? 'PDF',
// Shipper: Customer sends back to us (swap addresses)
'shipper' => [
'name' => trim(($recipient['firstname'] ?? '') . ' ' . ($recipient['lastname'] ?? '')),
'name2' => $recipient['company'] ?? '',
'street' => $recipient['street'] ?? '',
'houseNumber' => $recipient['houseNumber'] ?? '',
'postalCode' => $recipient['postalCode'] ?? '',
'city' => $recipient['city'] ?? '',
'country' => $recipient['country'] ?? 'DEU',
'email' => $recipient['email'] ?? '',
'phone' => $recipient['phone'] ?? '',
],
// Consignee: Our warehouse
'consignee' => [
'name' => $dhlConfig['sender']['company'] ?? 'mivita care gmbh',
'name2' => $dhlConfig['sender']['name'] ?? '',
'street' => $dhlConfig['sender']['street'] ?? 'Leinfeld',
'houseNumber' => $dhlConfig['sender']['house_number'] ?? '2',
'postalCode' => $dhlConfig['sender']['postalCode'] ?? '87755',
'city' => $dhlConfig['sender']['city'] ?? 'Kirchhaslach',
'country' => $dhlConfig['sender']['country'] ?? 'DEU',
'email' => $dhlConfig['sender']['email'] ?? 'versand@mivita.care',
'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789',
],
];
// Create the return label using ReturnsService
$result = $returnsService->createReturn($returnData);
Log::info('[DHL Controller] Return label created successfully (sync)', [
'original_shipment_id' => $shipment->id,
'return_shipment_number' => $result['returnNumber'] ?? 'N/A',
]);
return [
'success' => true,
'message' => 'Retourenlabel wurde erfolgreich erstellt!',
'shipment_number' => $result['returnNumber'] ?? null,
'return_shipment' => $result['returnShipment'] ?? null,
];
} catch (Exception $e) {
Log::error('[DHL Controller] Return label creation failed (sync)', [
'original_shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'Fehler beim Erstellen des Retourenlabels: ' . $e->getMessage(),
];
}
}
/**
* Update tracking status for the specified shipment
*
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function updateTracking(DhlShipment $shipment): JsonResponse
{
@ -473,12 +565,12 @@ class DhlShipmentController extends Controller
if (! $shipment->dhl_shipment_no) {
return response()->json([
'success' => false,
'message' => 'Keine DHL-Sendungsnummer verfügbar.'
'message' => 'Keine DHL-Sendungsnummer verfügbar.',
], 422);
}
// Use DhlTrackingService (handles queue/sync automatically based on config)
$dhlTrackingService = new DhlTrackingService();
$dhlTrackingService = new DhlTrackingService;
$result = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
Log::info('[DHL Controller] Tracking update processed', [
@ -497,16 +589,103 @@ class DhlShipmentController extends Controller
return response()->json([
'success' => false,
'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: ' . $e->getMessage()
'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: ' . $e->getMessage(),
], 500);
}
}
/**
* Send tracking email to customer (supports multiple shipments per order)
*/
public function sendTrackingEmail(DhlShipment $shipment): JsonResponse
{
try {
// Check if shipment has tracking number
if (! $shipment->dhl_shipment_no) {
return response()->json([
'success' => false,
'message' => 'Keine DHL-Sendungsnummer verfügbar.',
], 422);
}
// Check if shipment can send email
if (! $shipment->canSendTrackingEmail()) {
return response()->json([
'success' => false,
'message' => 'E-Mail kann nicht gesendet werden. Bestellung oder E-Mail-Adresse fehlt.',
], 422);
}
$order = $shipment->shoppingOrder;
// Determine recipient email: prefer shipment email, fallback to shopping user email
$recipientEmail = null;
if (! empty($shipment->email)) {
$recipientEmail = $shipment->email;
} elseif ($order->shopping_user && ! empty($order->shopping_user->email)) {
$recipientEmail = $order->shopping_user->email;
}
if (! $recipientEmail) {
return response()->json([
'success' => false,
'message' => 'Keine Empfänger-E-Mail-Adresse verfügbar.',
], 422);
}
// Collect all shipments for this order that have tracking numbers
$allShipments = DhlShipment::where('order_id', $order->id)
->whereNotNull('dhl_shipment_no')
->whereIn('status', ['created', 'in_transit', 'out_for_delivery'])
->orderBy('created_at', 'asc')
->get();
// If no shipments found, use only the current one
if ($allShipments->isEmpty()) {
$allShipments = collect([$shipment]);
}
// Send email with all shipments
Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order));
// Mark all included shipments as sent
foreach ($allShipments as $s) {
$s->markTrackingEmailSent('manual');
}
Log::info('[DHL Controller] Tracking email sent', [
'shipment_ids' => $allShipments->pluck('id')->toArray(),
'shipments_count' => $allShipments->count(),
'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(),
'email' => $recipientEmail,
'type' => 'manual',
]);
$message = $allShipments->count() > 1
? "Tracking-E-Mail mit {$allShipments->count()} Sendungen wurde erfolgreich an {$recipientEmail} gesendet."
: "Tracking-E-Mail wurde erfolgreich an {$recipientEmail} gesendet.";
return response()->json([
'success' => true,
'message' => $message,
'sent_at' => now()->format('d.m.Y H:i'),
'shipments_count' => $allShipments->count(),
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to send tracking email', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Senden der Tracking-E-Mail: ' . $e->getMessage(),
], 500);
}
}
/**
* Download shipping label
*
* @param DhlShipment $shipment
* @return Response
*/
public function downloadLabel(DhlShipment $shipment): Response
{
@ -538,9 +717,6 @@ class DhlShipmentController extends Controller
* Generate descriptive filename for DHL label
* Format: DHL-Kundenname-Sendungsnummer-Datum.pdf
* Example: DHL-Geraldine-Seebacher-0034043333301020015589177-15092025.pdf
*
* @param DhlShipment $shipment
* @return string
*/
private function generateLabelFilename(DhlShipment $shipment): string
{
@ -587,7 +763,6 @@ class DhlShipmentController extends Controller
/**
* Batch operations (multiple shipments)
*
* @param Request $request
* @return JsonResponse|BinaryFileResponse
*/
public function batchAction(Request $request)
@ -621,7 +796,7 @@ class DhlShipmentController extends Controller
case 'update_tracking':
if ($shipment->dhl_shipment_no) {
$dhlTrackingService = new DhlTrackingService();
$dhlTrackingService = new DhlTrackingService;
$trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
if ($trackingResult['success']) {
@ -639,7 +814,7 @@ class DhlShipmentController extends Controller
$labels[] = [
'shipment' => $shipment,
'filename' => $this->generateLabelFilename($shipment),
'path' => $shipment->label_path
'path' => $shipment->label_path,
];
$processed++;
} else {
@ -677,16 +852,13 @@ class DhlShipmentController extends Controller
return response()->json([
'success' => false,
'message' => 'Fehler bei der Stapelverarbeitung: ' . $e->getMessage()
'message' => 'Fehler bei der Stapelverarbeitung: ' . $e->getMessage(),
], 500);
}
}
/**
* Public tracking page (for customers)
*
* @param Request $request
* @return View|JsonResponse
*/
public function track(Request $request): View|JsonResponse
{
@ -701,12 +873,12 @@ class DhlShipmentController extends Controller
if (! $shipment) {
return response()->json([
'success' => false,
'message' => 'Sendung nicht gefunden.'
'message' => 'Sendung nicht gefunden.',
], 404);
}
// Use DhlTrackingService for tracking update
$dhlTrackingService = new DhlTrackingService();
$dhlTrackingService = new DhlTrackingService;
$trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
return response()->json([
@ -717,7 +889,7 @@ class DhlShipmentController extends Controller
'status' => $shipment->status,
'tracking_status' => $shipment->tracking_status,
'last_tracked_at' => $shipment->last_tracked_at?->format('d.m.Y H:i'),
]
],
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Public tracking failed', [
@ -727,7 +899,7 @@ class DhlShipmentController extends Controller
return response()->json([
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen.'
'message' => 'Fehler beim Abrufen der Tracking-Informationen.',
], 500);
}
}
@ -744,7 +916,7 @@ class DhlShipmentController extends Controller
private function createLabelsZip(array $labels)
{
try {
$zip = new ZipArchive();
$zip = new ZipArchive;
$zipFilename = 'dhl_labels_' . date('Y-m-d_H-i-s') . '.zip';
$zipPath = storage_path('app/temp/' . $zipFilename);
@ -753,7 +925,7 @@ class DhlShipmentController extends Controller
mkdir(storage_path('app/temp'), 0755, true);
}
if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
throw new Exception('ZIP-Datei konnte nicht erstellt werden.');
}
@ -779,19 +951,19 @@ class DhlShipmentController extends Controller
Log::info('[DHL Controller] Labels ZIP created', [
'zip_file' => $zipFilename,
'files_count' => $addedFiles,
'total_labels' => count($labels)
'total_labels' => count($labels),
]);
return response()->download($zipPath, $zipFilename)->deleteFileAfterSend(true);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to create labels ZIP', [
'error' => $e->getMessage(),
'labels_count' => count($labels)
'labels_count' => count($labels),
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen der ZIP-Datei: ' . $e->getMessage()
'message' => 'Fehler beim Erstellen der ZIP-Datei: ' . $e->getMessage(),
], 500);
}
}

View file

@ -39,6 +39,7 @@ class HomeController extends Controller
$data = [
'user' => Auth::user(),
'now' => Carbon::now(),
'dashboardNews' => \App\Models\DashboardNews::getActiveNews(),
];
return view('home', $data);
}

View file

@ -26,7 +26,6 @@ class LeadController extends Controller
{
$this->middleware('admin');
$this->userRepo = $userRepo;
}
/**
@ -49,7 +48,8 @@ class LeadController extends Controller
}
private function setFilterVars(){
private function setFilterVars()
{
/*if(!session('leads_filter_month')){
session(['leads_filter_month' => intval(date('m'))]);
@ -236,7 +236,6 @@ class LeadController extends Controller
$user = new User();
$user->id = "new";
$user->account = new UserAccount();
} else {
$user = User::findOrFail($data['user_id']);
if (!$user->account) {
@ -289,7 +288,8 @@ class LeadController extends Controller
return redirect(route('admin_lead_edit', [$user->id]) . "?show=" . $show);
}
//user released when register is complete
public function released($action, $id){
public function released($action, $id)
{
$user = User::findOrFail($id);
@ -354,21 +354,20 @@ class LeadController extends Controller
];
try {
Mail::to($user->email)->locale($user->getLocale())->send(new MailCustomMessage($user, $data, \Auth::user(), true));
}
catch(\Exception $e){
} catch (\Exception $e) {
dump($e->getMessage());
dd("error");
}
UserHistory::create(['user_id' => $user->id, 'action' => 'released_incomplete', 'status' => 0]);
\Session()->flash('alert-success', "E-Mail an Berater gesendet.");
}
return redirect(route('admin_lead_edit', [$user->id]));
}
//send new verfified mail to user
public function newMailVerified($id){
public function newMailVerified($id)
{
$user = User::findOrFail($id);
@ -380,8 +379,7 @@ class LeadController extends Controller
try {
Mail::to($user->email)->locale($user->getLocale())->send(new MailVerifyAccount($confirmation_code, $user));
}
catch(\Exception $e){
} catch (\Exception $e) {
dump($e->getMessage());
dd("error");
}
@ -389,11 +387,11 @@ class LeadController extends Controller
\Session()->flash('alert-success', "E-Mail erneut gesendet");
return redirect(route('admin_lead_edit', [$user->id]));
}
public function deleteFile($user_id, $file_id, $relation){
public function deleteFile($user_id, $file_id, $relation)
{
if ($relation === 'upload') {
$user = User::findOrFail($user_id);
@ -500,7 +498,6 @@ class LeadController extends Controller
})
->addColumn('shop_domain', function (User $user) {
return $user->shop ? ' <span class="small"><a href="' . $user->shop->getSubdomain(false) . '" target="_blank">' . $user->shop->getSubdomain(false) . '</a></span>' : '';
})
->addColumn('turnover', function (User $user) {
return "-";

View file

@ -65,21 +65,25 @@ class PayoneController extends Controller
private $reference;
public function __construct() {
public function __construct()
{
$this->default = \Config::get('payone.defaults');
}
public function init($shopping_user, $shopping_order){
public function init($shopping_user, $shopping_order)
{
$this->shopping_user = $shopping_user;
$this->shopping_order = $shopping_order;
$this->default['mode'] = $this->shopping_order->mode;
}
public function getShoppingPayment(){
public function getShoppingPayment()
{
return $this->shopping_payment;
}
public function setAboPayment($user_abo, $amount, $currency){
public function setAboPayment($user_abo, $amount, $currency)
{
$this->reference = substr(uniqid('m', false), 0, 16);
$this->method = [
@ -122,7 +126,8 @@ class PayoneController extends Controller
]);
}
//make Payone payment
public function setPrePayment($payment_method, $amount, $currency, $ret = []){
public function setPrePayment($payment_method, $amount, $currency, $ret = [])
{
$this->reference = substr(uniqid('m', false), 0, 16);
$this->setMethod($payment_method, $ret);
@ -149,7 +154,6 @@ class PayoneController extends Controller
'amount' => $amount,
];
$this->method['request'] = 'authorization';
}
if ($this->method["clearingtype"] === "wlt") {
@ -164,7 +168,6 @@ class PayoneController extends Controller
$this->setDeliverylData($this->shopping_user);
$this->method['request'] = 'authorization';
}
}
$this->shopping_payment = ShoppingPayment::create([
@ -186,7 +189,8 @@ class PayoneController extends Controller
return $this->reference;
}
public function setPersonalData(){
public function setPersonalData()
{
$this->personalData = [
"firstname" => $this->shopping_user->billing_firstname,
"lastname" => $this->shopping_user->billing_lastname, // mandatory
@ -210,10 +214,10 @@ class PayoneController extends Controller
"shipping_city" => "Frankfurt am Main",
"shipping_country" => "DE"
);*/
}
private function setMethod($payment_method, $ret = []){
private function setMethod($payment_method, $ret = [])
{
if ($payment_method) {
if (strpos($payment_method, '#')) {
@ -226,8 +230,6 @@ class PayoneController extends Controller
'onlinebanktransfertype' => "",
"request" => "authorization"
];
}
//Online-Überweisung
if ($payment_method[0] === 'sb') {
@ -262,7 +264,6 @@ class PayoneController extends Controller
"add_paydata[payment_type]" => "Payolution-Invoicing",
];*/
}
}
//vorkasse
if ($payment_method === 'elv') {
@ -304,16 +305,17 @@ class PayoneController extends Controller
}
}
public function onlyPaymentResponse(){
public function onlyPaymentResponse()
{
$request = array_merge($this->default, $this->personalData, $this->deliveryData, $this->method, $this->prepayment, $this->aboInitPayment, $this->urls);
$response = Payone::sendRequest($request);
return $response;
}
public function ResponseData($is_abo = false){
public function ResponseData($is_abo = false)
{
$request = array_merge($this->default, $this->personalData, $this->deliveryData, $this->method, $this->prepayment, $this->aboInitPayment, $this->urls);
//dd($request);
//RECHNUNG MIV
if ($this->shopping_payment->clearingtype === 'fnc' && $this->shopping_payment->onlinebanktransfertype === 'MIV') {
$payt = PaymentTransaction::create([
@ -333,7 +335,6 @@ class PayoneController extends Controller
return redirect(route('checkout.transaction_approved', [$payt->id, $this->reference]));
exit;
}
$response = Payone::sendRequest($request);
/*
* status APPROVED / REDIRECT / ERROR / PENDING
@ -380,7 +381,6 @@ class PayoneController extends Controller
}
return redirect()->away($response["redirecturl"]);
exit;
}
if ($response['status'] === 'APPROVED') {
@ -489,7 +489,8 @@ class PayoneController extends Controller
return Payone::sendRequest($request);
}
public function setDeliverylData($shopping_user){
public function setDeliverylData($shopping_user)
{
if ($shopping_user->same_as_billing == true) {
$this->deliveryData = [
'shipping_firstname' => $shopping_user->billing_firstname,
@ -597,11 +598,4 @@ class PayoneController extends Controller
*/
}

View file

@ -4,14 +4,13 @@ namespace App\Http\Controllers;
use App\Models\Country;
use App\Models\Product;
use App\Models\ProductBundle;
use App\Models\ProductImage;
use App\Models\ProductIngredient;
use App\Repositories\ProductRepository;
use Request;
use Validator;
class ProductController extends Controller
{
protected $productRepo;
@ -20,7 +19,6 @@ class ProductController extends Controller
{
$this->middleware('admin');
$this->productRepo = $productRepo;
}
public function index()
@ -28,22 +26,22 @@ class ProductController extends Controller
if (Request::get('show_active_products')) {
set_user_attr('show_active_products', Request::get('show_active_products'));
}
if(get_user_attr('show_active_products') === "true"){
if (get_user_attr('show_active_products') === 'true') {
$values = Product::where('active', true)->orderBy('pos', 'DESC')->orderBy('id', 'DESC')->get();
} else {
$values = Product::orderBy('pos', 'DESC')->orderBy('id', 'DESC')->get();
}
$data = [
'values' => $values
'values' => $values,
];
return view('admin.product.index', $data);
}
public function edit($id)
{
if($id === "new"){
$model = new Product();
if ($id === 'new') {
$model = new Product;
$model->active = true;
} else {
$model = Product::findOrFail($id);
@ -53,31 +51,31 @@ class ProductController extends Controller
'product' => $model,
'country_for_prices' => $country_for_prices,
];
return view('admin.product.edit', $data);
}
public function store()
{
$data = Request::all();
$rules = array(
$rules = [
'name' => 'required',
);
];
/*if(isset($data['number']) && $data['number'] != ""){
$rules['number'] = 'int';
}*/
if (isset($data['wp_number'])) {
if($data['id'] !== "new"){
if ($data['id'] !== 'new') {
$model = Product::findOrFail($data['id']);
$rules['wp_number'] = 'unique:products,wp_number,'.$model->id;
} else {
$rules['wp_number'] = 'unique:products,wp_number';
}
}
$validator = Validator::make(Request::all(), $rules);
if($data['id'] === "new"){
$model = new Product();
if ($data['id'] === 'new') {
$model = new Product;
} else {
$model = Product::findOrFail($data['id']);
}
@ -92,35 +90,36 @@ class ProductController extends Controller
if ($validator->fails()) {
return view('admin.product.edit', $data)->withErrors($validator);
} else {
$product = $this->productRepo->update(Request::all());
\Session()->flash('alert-save', true);
return redirect(route('admin_product_edit', [$product->id]));
}
\Session()->flash('alert-save', '1');
return redirect(route('admin_product_show'));
}
public function copy($id){
public function copy($id)
{
$model = Product::findOrFail($id);
$product = $this->productRepo->copy($model);
\Session()->flash('alert-success', 'Eintrag kopiert');
return redirect(route('admin_product_show'));
}
public function delete($id, $do = 'product', $did = null){
public function delete($id, $do = 'product', $did = null)
{
if ($do === 'product') {
$model = Product::findOrFail($id);
$model->delete();
\Session()->flash('alert-success', 'Eintrag gelöscht');
return redirect(route('admin_product_show'));
}
@ -130,17 +129,27 @@ class ProductController extends Controller
if ($ProductIngredient) {
$ProductIngredient->delete();
\Session()->flash('alert-success', 'Eintrag gelöscht');
return redirect(route('admin_product_edit', [$model->id]));
}
}
}
if ($do === 'bundle') {
$model = Product::findOrFail($id);
$ProductBundle = ProductBundle::where('bundle_product_id', $did)->where('product_id', $model->id)->first();
if ($ProductBundle) {
$ProductBundle->delete();
\Session()->flash('alert-success', 'Bundle-Produkt entfernt');
return redirect(route('admin_product_edit', [$model->id]));
}
}
}
// Upload FILE -----------------------------------------------------------------------------------------------------------------------
public function imageUpload(){
public function imageUpload()
{
$product_id = Request::get('product_id');
$product = Product::findOrFail($product_id);
@ -148,15 +157,15 @@ class ProductController 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 = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (! isset($file_ex[$image['output']['type']])) {
\Session()->flash('alert-danger', 'File is not jpg or png!');
return redirect(route('admin_product_edit', [$product->id]));
}
@ -177,24 +186,25 @@ class ProductController extends Controller
'original_name' => $image['output']['name'],
'ext' => $ext,
'mine' => $image['output']['type'],
'size' => $image['input']['size']
'size' => $image['input']['size'],
]);
\Session()->flash('alert-success', __('msg.file_uploaded'));
return redirect(route('admin_product_edit', [$product->id]));
}
\Session()->flash('alert-danger', __('msg.file_empty'));
return redirect(route('admin_product_edit', [$product->id]));
}
catch ( \Exception $e) {
\Session()->flash('alert-danger', "Fehler".$e);
return redirect(route('admin_product_edit', [$product->id]));
} catch (\Exception $e) {
\Session()->flash('alert-danger', 'Fehler'.$e);
return redirect(route('admin_product_edit', [$product->id]));
}
}
public function imageDelete($image_id, $product_id){
public function imageDelete($image_id, $product_id)
{
$product = Product::findOrFail($product_id);
$product_image = ProductImage::findOrFail($image_id);
@ -206,15 +216,16 @@ class ProductController extends Controller
$product_image->delete();
\Session()->flash('alert-success', __('msg.file_deleted'));
return redirect(route('admin_product_edit', [$product->id]));
return redirect(route('admin_product_edit', [$product->id]));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
return redirect(route('admin_product_edit', [$product->id]));
return redirect(route('admin_product_edit', [$product->id]));
}
public function imageAttribute($product_id, $attr, $val = false){
public function imageAttribute($product_id, $attr, $val = false)
{
if (is_numeric($val) && $val < 0) {
$val = 0;
@ -225,9 +236,8 @@ class ProductController extends Controller
$product_image->{$attr} = $val;
$product_image->save();
\Session()->flash('alert-success', "Wert gespeichert");
\Session()->flash('alert-success', 'Wert gespeichert');
return redirect()->back();
}
}

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\IqImage;
use App\Models\IqSite;
use App\Models\DashboardNews;
use Request;
@ -20,6 +21,98 @@ class SitesController extends Controller
//
}
// Dashboard News Methods
public function dashboardNews()
{
$data = [
'news' => DashboardNews::orderBy('created_at', 'DESC')->get(),
'languages' => config('localization.supportedLocales'),
];
return view('admin.site.news.index', $data);
}
public function dashboardNewsEdit($id)
{
$news = $id === 'new' ? new DashboardNews() : DashboardNews::findOrFail($id);
$data = [
'news' => $news,
'languages' => config('localization.supportedLocales'),
];
return view('admin.site.news.edit', $data);
}
public function dashboardNewsStore($id)
{
$data = Request::all();
// Handle translations
$transTitle = [];
$transTeaser = [];
$transContent = [];
foreach (config('localization.supportedLocales') as $locale => $localeData) {
if ($locale !== 'de') {
$transTitle[$locale] = Request::get('trans_title_' . $locale, '');
$transTeaser[$locale] = Request::get('trans_teaser_' . $locale, '');
$transContent[$locale] = Request::get('trans_content_' . $locale, '');
}
}
$data['trans_title'] = $transTitle;
$data['trans_teaser'] = $transTeaser;
$data['trans_content'] = $transContent;
$data['active'] = Request::has('active') ? 1 : 0;
// Handle file links
$fileLinks = Request::get('file_links', []);
// Filter out empty entries (where neither label nor file_id is set)
$filteredFileLinks = [];
foreach ($fileLinks as $locale => $links) {
if (is_array($links)) {
$filteredFileLinks[$locale] = array_values(array_filter($links, function ($link) {
return !empty($link['file_id']) || !empty($link['label']);
}));
}
}
$data['file_links'] = $filteredFileLinks;
// Handle display_date
if (!empty($data['display_date'])) {
try {
$data['display_date'] = \Carbon\Carbon::createFromFormat('d.m.Y', $data['display_date'])->format('Y-m-d');
} catch (\Throwable $e) {
$data['display_date'] = now()->format('Y-m-d');
}
} else {
$data['display_date'] = now()->format('Y-m-d');
}
// Wenn diese News aktiv gesetzt wird, setze alle anderen auf inaktiv
if ($data['active']) {
DashboardNews::where('active', 1)->update(['active' => 0]);
}
if ($id === 'new') {
DashboardNews::create($data);
} else {
$news = DashboardNews::findOrFail($id);
$news->fill($data);
$news->save();
}
\Session()->flash('alert-success', __('msg.saved_successfully'));
return redirect(route('admin_dashboard_news'));
}
public function dashboardNewsDelete($id)
{
$news = DashboardNews::findOrFail($id);
$news->delete();
\Session()->flash('alert-success', __('msg.deleted_successfully'));
return redirect(route('admin_dashboard_news'));
}
public function show($site)
{
$data = [
@ -45,21 +138,20 @@ class SitesController extends Controller
\Session()->flash('alert-save', '1');
return redirect(route('admin_sites', ['start']));
}
// Upload FILE -----------------------------------------------------------------------------------------------------------------------
public function imageUpload($site){
public function imageUpload($site)
{
$model = IqSite::find(1);
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'];
@ -102,15 +194,14 @@ class SitesController extends Controller
}
\Session()->flash('alert-danger', __('msg.file_empty'));
return redirect(route('admin_sites', [$model->slug]));
}
catch (Exception $e) {
} catch (\Exception $e) {
\Session()->flash('alert-danger', "Error: " . $e);
return redirect(route('admin_sites', [$model->slug]));
}
}
public function imageDelete($site, $image_id){
public function imageDelete($site, $image_id)
{
$iq_image = IqImage::findOrFail($image_id);
$model = IqSite::find(1);
@ -126,14 +217,13 @@ class SitesController extends Controller
\Session()->flash('alert-success', __('msg.file_deleted'));
return redirect(route('admin_sites', [$model->slug]));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
return redirect(route('admin_sites', [$model->slug]));
}
public function imageAttribute($site, $image_id, $attr, $val = false){
public function imageAttribute($site, $image_id, $attr, $val = false)
{
$iq_image = IqImage::findOrFail($image_id);
@ -142,7 +232,5 @@ class SitesController extends Controller
\Session()->flash('alert-success', "Wert gespeichert");
return redirect()->back();
}
}

View file

@ -270,7 +270,9 @@ class AboController extends Controller
->addColumn('name', function (Product $product) use ($user_abo) {
return '<strong>' . $product->getLang('name') . '</strong><br>' . get_abo_type_badge_by_product($product);
})
->addColumn('points', function (Product $product) use ($user_abo) {
return '<span class="no-line-break">' . $product->getFormattedPoints() . '</span>';
})
->addColumn('price_net', function (Product $product) use ($user_abo) {
$ufactor = $user_abo->is_for === 'me' ? true : false;
return '<span class="no-line-break">' . $product->getFormattedPriceWith(true, $ufactor, Yard::instance('shopping')->getUserCountry()) . " €</span>" . '<span class="no-line-break">' . $product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()) . '</span>';
@ -298,7 +300,7 @@ class AboController extends Controller
->orderColumn('contents_total', 'contents_total $1')
->orderColumn('weight', 'weight $1')
->rawColumns(['add_card', 'product', 'name', 'quantity', 'picture', 'price_net', 'price_gross', 'action'])
->rawColumns(['add_card', 'points', 'product', 'name', 'quantity', 'picture', 'price_net', 'price_gross', 'action'])
->make(true);
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\User;
use Auth;
use Yard;
use Request;
@ -176,7 +177,8 @@ class HomepartyController extends Controller
return redirect(route('user_homeparty_detail', [$homeparty->id, $step]));
}
private function storeTranslations($homeparty, $lang, $data){
private function storeTranslations($homeparty, $lang, $data)
{
if ($lang == 'de') {
$homeparty->description = $data['description'];
@ -299,7 +301,8 @@ class HomepartyController extends Controller
return view('user.homeparty.order', $data);
}
private function userChangeCountry($homeparty){
private function userChangeCountry($homeparty)
{
if (isset($homeparty->card_info['user_country_id'])) {
if ($homeparty->card_info['user_country_id'] !== UserService::$user_country->id) {
// es wurde schon eine order angelegt, aber das Rechungsland geändert
@ -314,7 +317,8 @@ class HomepartyController extends Controller
return false;
}
private function checkShoppingCountry($country_id){
private function checkShoppingCountry($country_id)
{
if ($country_id) {
if ($shipping_country = ShippingCountry::whereCountryId($country_id)->first()) {
return $shipping_country->id;
@ -379,9 +383,7 @@ class HomepartyController extends Controller
]);
}
}
}
}
$homeparty_user = HomepartyUser::findOrFail($data['homeparty_user_id']);
HomepartyCart::calculateHomeparty($homeparty);
@ -543,7 +545,6 @@ class HomepartyController extends Controller
$homeparty_user->delete();
\Session()->flash('alert-success', __('msg.homeparty_guest_delete'));
return redirect(route('user_homeparty_guests', [$homeparty->id]));
}
if ($do === 'hp') {
@ -566,12 +567,12 @@ class HomepartyController extends Controller
$homeparty->delete();
\Session()->flash('alert-success', __('msg.homeparty_delete'));
return redirect(route('user_homepartys'));
}
abort(404);
}
private function getHomparty($id){
private function getHomparty($id)
{
$homeparty = Homeparty::findOrFail($id);
if ($homeparty->auth_user_id !== \Auth::user()->id) {
abort(404);
@ -579,7 +580,8 @@ class HomepartyController extends Controller
return $homeparty;
}
public function datatable($homeparty_id){
public function datatable($homeparty_id)
{
$query = Product::select('products.*')->where('active', true)->whereJsonContains('show_on', '4');
$homeparty = Homeparty::findOrFail($homeparty_id);
@ -596,7 +598,6 @@ class HomepartyController extends Controller
<strong>&euro; ' . $product->getFormattedPriceWith(false, false, $homeparty->getUserCountry()) . '</strong>&nbsp; +<span class="ion ion-md-cart"></span>
</button>';
}
})
->addColumn('picture', function (Product $product) {
if (count($product->images)) {
@ -609,6 +610,9 @@ class HomepartyController extends Controller
'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, $homeparty->getUserCountry()).'</span>';
})
*/
->addColumn('points', function (Product $product) use ($homeparty) {
return '<span class="no-line-break">' . $product->getFormattedPoints() . '</span>';
})
->addColumn('price_gross', function (Product $product) use ($homeparty) {
if ($homeparty->getCardInfo('user_tax_free')) {
return '<span class="no-line-break">' . $product->getFormattedPriceWith(true, true, $homeparty->getUserCountry()) . " €</span>" .
@ -647,8 +651,7 @@ class HomepartyController extends Controller
->orderColumn('contents_total', 'contents_total $1')
->orderColumn('weight', 'weight $1')
->rawColumns(['add_card', 'product', 'quantity', 'picture', 'price_net', 'price_gross', 'price_vk_gross', 'action'])
->rawColumns(['add_card', 'points', 'product', 'quantity', 'picture', 'price_net', 'price_gross', 'price_vk_gross', 'action'])
->make(true);
}
}

View file

@ -51,7 +51,6 @@ class OrderController extends Controller
}
if ($shopping_order->payment_for === 6 || $shopping_order->payment_for === 7) {
Log::channel(self::LOG_CHANNEL)->info("Redirecting user #{$user->id} to customer order detail for order #{$id}");
return redirect(route('user_shop_order_detail', [$shopping_order->id]));
}
@ -134,14 +133,13 @@ class OrderController extends Controller
if (!Shop::checkShoppingCountry($for, $delivery_id) && !\Session()->has('custom-error')) {
$country = Shop::getDeliveryCountry($for, $delivery_id);
\Session()->flash('custom-error', $country . ": " . __('validation.custom.shipping_not_found'));
Log::channel(self::LOG_CHANNEL)->warning("Shipping country not found for user #{$user->id}, country: {$country}");
Log::channel(self::LOG_CHANNEL)->error("Shipping country not found for user #{$user->id}, country: {$country}");
return redirect(route('user_order_my_delivery', [$for, $delivery_id]));
}
if ($for === 'abo-ot-customer') {
if (AboHelper::hasAboByEmail($shopping_user->billing_email) && !\Session()->has('custom-error')) {
\Session()->flash('custom-error', __('abo.error_email_has_abo', ['email' => $shopping_user->billing_email]));
Log::channel(self::LOG_CHANNEL)->info("User #{$user->id} attempted to create abo for email that already has one: {$shopping_user->billing_email}");
return redirect(route('user_order_my_delivery', [$for, $delivery_id]));
}
}
@ -169,7 +167,7 @@ class OrderController extends Controller
$user = User::find(Auth::user()->id);
if ($for === 'abo-me' && AboHelper::userHasAbo($user)) {
Log::channel(self::LOG_CHANNEL)->warning("User #{$user->id} attempted to create abo but already has one");
Log::channel(self::LOG_CHANNEL)->error("User #{$user->id} attempted to create abo but already has one");
abort(403, 'User has an Abo. Cannot order.');
}
@ -225,7 +223,6 @@ class OrderController extends Controller
$validator = Validator::make(Request::all(), $rules);
if ($validator->fails()) {
Log::channel(self::LOG_CHANNEL)->info("Validation failed for payment form", ['errors' => $validator->errors()->toArray()]);
return back()->withErrors($validator)->withInput(Request::all());
}
@ -243,16 +240,10 @@ class OrderController extends Controller
if (Yard::instance('shopping')->getNumComp() > 0) {
if (!isset($data['switchers-comp-product'])) {
$validator->errors()->add('switchers-comp-product', __('msg.please_select_compensation_product'));
Log::channel(self::LOG_CHANNEL)->info("Compensation product not selected");
} else if (!is_array($data['switchers-comp-product'])) {
$validator->errors()->add('switchers-comp-product', __('msg.please_select_compensation_product'));
Log::channel(self::LOG_CHANNEL)->info("Compensation product selection is not an array");
} else if (count($data['switchers-comp-product']) !== Yard::instance('shopping')->getNumComp()) {
$validator->errors()->add('switchers-comp-product', __('mdg.please_select_count_compensation_products', ['count' => Yard::instance('shopping')->getNumComp()]));
Log::channel(self::LOG_CHANNEL)->info("Incorrect number of compensation products selected", [
'required' => Yard::instance('shopping')->getNumComp(),
'selected' => count($data['switchers-comp-product'])
]);
}
if ($validator->errors()->count()) {
@ -278,12 +269,6 @@ class OrderController extends Controller
unset($data['quantity']);
unset($data['_token']);
Log::channel(self::LOG_CHANNEL)->info("Processing payment for user #{$user->id}", [
'for' => $for,
'identifier' => $identifier,
'is_abo' => $data['is_abo']
]);
if ($for === 'ot-customer' || $for === 'abo-ot-customer') {
return $this->processCustomerPayment($user, $identifier, $data, $id, $for);
} else {
@ -316,10 +301,6 @@ class OrderController extends Controller
// Send Mail to Customer
try {
$this->customPaymentSendMail($user, $identifier, $yard_shopping_items, $data);
Log::channel(self::LOG_CHANNEL)->info("Custom payment email sent successfully", [
'identifier' => $identifier,
'user_id' => $user->id
]);
} catch (\Exception $e) {
Log::channel(self::LOG_CHANNEL)->error("Failed to send custom payment email: " . $e->getMessage(), [
'identifier' => $identifier,
@ -562,7 +543,6 @@ class OrderController extends Controller
throw new \Exception(__('msg.shipping_costs_were_not_calculated_correctly'));
}
}
}
}
@ -582,11 +562,13 @@ class OrderController extends Controller
$q->orWhereJsonContains('show_on', $id);
}
})
->orderByRaw("CASE
->orderByRaw(
"CASE
WHEN JSON_CONTAINS(show_on, ?, '$') THEN 1
WHEN JSON_CONTAINS(show_on, ?, '$') THEN 2
ELSE 3 END",
[$show_on_ids[0], isset($show_on_ids[1]) ? $show_on_ids[1] : $show_on_ids[0]]);
[$show_on_ids[0], isset($show_on_ids[1]) ? $show_on_ids[1] : $show_on_ids[0]]
);
} else {
$show_on_ids = $isAbo ? ['12', '13'] : ['3'];
@ -597,19 +579,15 @@ class OrderController extends Controller
$q->orWhereJsonContains('show_on', $id);
}
})
->orderByRaw("CASE
->orderByRaw(
"CASE
WHEN JSON_CONTAINS(show_on, ?, '$') THEN 1
WHEN JSON_CONTAINS(show_on, ?, '$') THEN 2
ELSE 3 END",
[$show_on_ids[0], isset($show_on_ids[1]) ? $show_on_ids[1] : $show_on_ids[0]]);
[$show_on_ids[0], isset($show_on_ids[1]) ? $show_on_ids[1] : $show_on_ids[0]]
);
}
Log::channel(self::LOG_CHANNEL)->info("Datatable query executed", [
'is_abo' => $isAbo,
'shipping_is_for' => $shippingIsFor,
'show_on_ids' => $show_on_ids
]);
return \DataTables::eloquent($query)
->addColumn('product', function (Product $product) {
$cartItem = Yard::instance('shopping')->getCartItemByProduct($product->id);
@ -637,6 +615,9 @@ class OrderController extends Controller
}
return "";
})
->addColumn('points', function (Product $product) {
return '<span class="no-line-break">' . $product->getFormattedPoints() . '</span>';
})
->addColumn('price_net', function (Product $product) {
return '<span class="no-line-break">' . $product->getFormattedPriceWith(true, true, Yard::instance('shopping')->getUserCountry()) . " €</span>" . '<span class="no-line-break">' . $product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()) . '</span>';
})
@ -678,7 +659,7 @@ class OrderController extends Controller
->orderColumn('contents_total', 'contents_total $1')
->orderColumn('weight', 'weight $1')
->orderColumn('abo', 'show_on $1')
->rawColumns(['add_card', 'price_net', 'price_gross', 'price_vk_gross', 'customer_price_net', 'customer_price_gross', 'my_commission_net', 'product', 'quantity', 'picture', 'abo', 'action'])
->rawColumns(['add_card', 'points', 'price_net', 'price_gross', 'price_vk_gross', 'customer_price_net', 'customer_price_gross', 'my_commission_net', 'product', 'quantity', 'picture', 'abo', 'action'])
->make(true);
}
@ -708,7 +689,6 @@ class OrderController extends Controller
if ($data['action'] === 'clearCart') {
Yard::instance('shopping')->destroy();
Log::channel(self::LOG_CHANNEL)->info("Cart cleared");
return response()->json(['response' => true, 'data' => Yard::instance('shopping')->count(), 'html_card' => '', 'html_comp' => '']);
}
@ -743,14 +723,26 @@ class OrderController extends Controller
// Get the cart item
if ($is_for === 'ot-customer' || $is_for === 'abo-ot-customer') {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), 1,
round($product->getPriceWith(Yard::instance('shopping')->getUserTaxFree(), false, Yard::instance('shopping')->getUserCountry()), 1), false, false,
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
->add(
$product->id,
$product->getLang('name'),
1,
round($product->getPriceWith(Yard::instance('shopping')->getUserTaxFree(), false, Yard::instance('shopping')->getUserCountry()), 1),
false,
false,
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
} else {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), 1,
$product->getPriceWith(Yard::instance('shopping')->getUserTaxFree(), true, Yard::instance('shopping')->getUserCountry()), false, false,
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
->add(
$product->id,
$product->getLang('name'),
1,
$product->getPriceWith(Yard::instance('shopping')->getUserTaxFree(), true, Yard::instance('shopping')->getUserCountry()),
false,
false,
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
}
if (Yard::instance('shopping')->getUserTaxFree()) {
@ -761,18 +753,9 @@ class OrderController extends Controller
if (isset($data['qty']) && $data['qty'] > 0) {
Yard::instance('shopping')->update($cartItem->rowId, $data['qty']);
Log::channel(self::LOG_CHANNEL)->info("Cart item updated", [
'product_id' => $product->id,
'product_name' => $product->getLang('name'),
'qty' => $data['qty']
]);
} else {
// If 0 get the item by qty:1 and remove it
Yard::instance('shopping')->remove($cartItem->rowId);
Log::channel(self::LOG_CHANNEL)->info("Cart item removed", [
'product_id' => $product->id,
'product_name' => $product->getLang('name')
]);
}
Yard::instance('shopping')->reCalculateShippingPrice();
@ -794,11 +777,6 @@ class OrderController extends Controller
if ($shipping_country) {
Yard::instance('shopping')->setShippingCountryWithPrice($shipping_country->id, $is_for);
$this->checkCompProduct(Yard::instance('shopping')->getNumComp());
Log::channel(self::LOG_CHANNEL)->info("Shipping country updated", [
'shipping_country_id' => $shipping_country->id,
'shipping_country_name' => $shipping_country->name ?? 'unknown'
]);
} else {
Log::channel(self::LOG_CHANNEL)->warning("Shipping country not found", [
'shipping_country_id' => $data['shipping_country_id']
@ -820,12 +798,6 @@ class OrderController extends Controller
$this->updateCompProduct($data);
Yard::instance('shopping')->reCalculateShippingPrice();
Log::channel(self::LOG_CHANNEL)->info("Compensation product updated", [
'comp_product_id' => $data['comp_product_id'] ?? null,
'comp_num' => $data['comp_num'] ?? null,
'count_comp_products' => $data['count_comp_products'] ?? null
]);
$html_card = view("user.order.yard_view_form", $data)->render();
$html_comp = view("user.order.comp_product", $data)->render();
@ -841,12 +813,6 @@ class OrderController extends Controller
// If equal or greater, delete due to new shipping costs
if ($row->options->comp && $row->options->comp > intval($count_comp_products)) {
Yard::instance('shopping')->remove($row->rowId);
Log::channel(self::LOG_CHANNEL)->info("Compensation product removed due to count change", [
'product_id' => $row->id,
'product_name' => $row->name,
'comp_value' => $row->options->comp,
'required_comp' => $count_comp_products
]);
}
}
}
@ -864,13 +830,6 @@ class OrderController extends Controller
//comp_num welches comp product wird hinzugefügt
if ($row->options->comp && ($row->options->comp == intval($data['comp_num']) || $row->options->comp > intval($data['count_comp_products']))) {
Yard::instance('shopping')->remove($row->rowId);
Log::channel(self::LOG_CHANNEL)->info("Compensation product removed during update", [
'product_id' => $row->id,
'product_name' => $row->name,
'comp_value' => $row->options->comp,
'comp_num' => $data['comp_num'],
'count_comp_products' => $data['count_comp_products']
]);
}
}
@ -881,7 +840,14 @@ class OrderController extends Controller
if ($product->images->count()) {
$image = $product->images->first()->slug;
}
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, 0, false, false, [
$cartItem = Yard::instance('shopping')->add(
$product->id,
$product->getLang('name'),
1,
0,
false,
false,
[
'image' => $image,
'slug' => $product->slug,
'weight' => 0,
@ -892,12 +858,6 @@ class OrderController extends Controller
);
Yard::setTax($cartItem->rowId, 0);
Log::channel(self::LOG_CHANNEL)->info("Compensation product added", [
'product_id' => $product->id,
'product_name' => $product->getLang('name'),
'comp_num' => $data['comp_num']
]);
} else {
Log::channel(self::LOG_CHANNEL)->warning("Compensation product not found", [
'comp_product_id' => $data['comp_product_id']
@ -913,7 +873,6 @@ class OrderController extends Controller
{
try {
$data = OrderPaymentService::getCustomPayment($identifier);
Log::channel(self::LOG_CHANNEL)->info("Custom payment page accessed", ['identifier' => $identifier]);
return view('user.order.payment.custom_payment', $data);
} catch (\Exception $e) {
Log::channel(self::LOG_CHANNEL)->error("Error accessing custom payment: " . $e->getMessage(), ['identifier' => $identifier]);
@ -951,12 +910,6 @@ class OrderController extends Controller
$bcc[] = $data['mode'] === 'test' ? config('app.checkout_test_mail') : config('app.checkout_mail');
$bcc[] = $shopping_user->member ? $shopping_user->member->email : $user->email;
Log::channel(self::LOG_CHANNEL)->info("Sending custom payment email", [
'to' => $billing_email,
'bcc' => $bcc,
'identifier' => $identifier
]);
Mail::to($billing_email)
->bcc($bcc)
->locale(\App::getLocale())

View file

@ -13,6 +13,7 @@ use App\Services\BusinessPlan\TreeCalcBot;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\Services\BusinessPlan\TreeHelperOptimized;
use App\Services\HTMLHelper;
use App\Services\LevelReportService;
use App\Services\NextLevelBadgeHelper;
use App\Services\TranslationHelper;
use App\User;
@ -134,7 +135,7 @@ class TeamController extends Controller
$user = User::find(\Auth::user()->id);
if (config('app.debug')) {
$user = User::find(454);
// $user = User::find(454);
}
$this->setFilterVars();
@ -491,6 +492,141 @@ class TeamController extends Controller
}
}
/**
* Zeigt Level-Aufstieg Reports für das eigene Team
* Nur für einen ausgewählten Monat/Jahr basierend auf TreeCalcBotOptimized
*/
public function levelReports(Request $request)
{
$startTime = microtime(true);
try {
$user = User::find(\Auth::user()->id);
$this->setFilterVars();
// Monat und Jahr aus Request oder Session
$month = Request::get('month') ?: session('team_user_filter_month_prev', intval(date('m') - 1));
$year = Request::get('year') ?: session('team_user_filter_year', date('Y'));
$onlyNotUpdated = Request::boolean('not_updated', false);
// Prüfe ob Live-Berechnung erzwungen werden soll
$forceLiveCalculation = false; //Request::get('force_live_calculation', false) || Request::get('live', false);
\Log::info("TeamController: Building level reports for user {$user->id} ({$month}/{$year})");
// Lade Team-Struktur mit TreeCalcBotOptimized
$treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', $forceLiveCalculation);
$treeCalcBot->initStructureUser($user->id, $forceLiveCalculation);
// Lade Level-Reports für Team
$levelReportService = new LevelReportService();
$filters = ['only_not_updated' => $onlyNotUpdated];
$promotions = $levelReportService->getTeamLevelPromotions($treeCalcBot, $month, $year, $filters);
$statistics = $levelReportService->getStatistics($promotions);
$endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
\Log::info("TeamController: Level reports loaded for user {$user->id} in {$executionTime}ms - " . $promotions->count() . " promotions found");
$availableYears = range(date('Y'), date('Y') - 5);
$availableMonths = [
1 => __('cal.months.January'),
2 => __('cal.months.February'),
3 => __('cal.months.March'),
4 => __('cal.months.April'),
5 => __('cal.months.May'),
6 => __('cal.months.June'),
7 => __('cal.months.July'),
8 => __('cal.months.August'),
9 => __('cal.months.September'),
10 => __('cal.months.October'),
11 => __('cal.months.November'),
12 => __('cal.months.December')
];
$data = [
'promotions' => $promotions,
'statistics' => $statistics,
'filters' => [
'month' => $month,
'year' => $year,
'only_not_updated' => $onlyNotUpdated
],
'availableYears' => $availableYears,
'availableMonths' => $availableMonths,
'performance' => [
'execution_time' => $executionTime,
'user_id' => $user->id
]
];
return view('user.team.level-reports', $data);
} catch (\Exception $e) {
\Log::error("TeamController: Error loading level reports: " . $e->getMessage());
return view('user.team.level-reports', [
'error' => 'Fehler beim Laden der Level-Reports: ' . $e->getMessage(),
'promotions' => collect([]),
'statistics' => ['total_count' => 0, 'level_stats' => [], 'period_stats' => []],
'filters' => ['month' => date('m'), 'year' => date('Y'), 'only_not_updated' => false],
'availableYears' => range(date('Y'), date('Y') - 5),
'availableMonths' => [
1 => __('cal.months.January'),
2 => __('cal.months.February'),
3 => __('cal.months.March'),
4 => __('cal.months.April'),
5 => __('cal.months.May'),
6 => __('cal.months.June'),
7 => __('cal.months.July'),
8 => __('cal.months.August'),
9 => __('cal.months.September'),
10 => __('cal.months.October'),
11 => __('cal.months.November'),
12 => __('cal.months.December')
]
]);
}
}
/**
* CSV Export für Team Level-Reports
*/
public function levelReportsExport(Request $request)
{
try {
$user = User::find(\Auth::user()->id);
$this->setFilterVars();
$month = Request::get('month') ?: session('team_user_filter_month_prev', intval(date('m') - 1));
$year = Request::get('year') ?: session('team_user_filter_year', date('Y'));
$onlyNotUpdated = Request::boolean('not_updated', false);
$forceLiveCalculation = Request::get('force_live_calculation', false) || Request::get('live', false);
// Lade Team-Struktur
$treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', $forceLiveCalculation);
$treeCalcBot->initStructureUser($user->id, $forceLiveCalculation);
// Lade Level-Reports
$levelReportService = new LevelReportService();
$filters = ['only_not_updated' => $onlyNotUpdated];
$promotions = $levelReportService->getTeamLevelPromotions($treeCalcBot, $month, $year, $filters);
if ($promotions->isEmpty()) {
return redirect()->back()->with('error', 'Keine Daten für Export gefunden.');
}
// CSV erstellen
$filename = 'team_level_promotions_' . date('Y-m-d_H-i-s') . '.csv';
$filepath = $levelReportService->exportToCsv($promotions, $filename);
return response()->download($filepath, $filename)->deleteFileAfterSend(true);
} catch (\Exception $e) {
\Log::error("TeamController: Error exporting level reports: " . $e->getMessage());
return redirect()->back()->with('error', 'Fehler beim Export: ' . $e->getMessage());
}
}
/**
* Zeigt den Marketingplan für User an
* Übersichtliche Darstellung aller Karriere-Level mit wichtigen Informationen
@ -553,6 +689,94 @@ class TeamController extends Controller
return view('user.team.members', $data);
}
/**
* Zeigt die Abos der Team-Mitglieder an
*/
public function showAbos()
{
$user = User::find(\Auth::user()->id);
$this->setFilterVars();
// Nutze TreeCalcBotOptimized um das Team zu bekommen
$month = session('team_user_filter_month');
$year = session('team_user_filter_year');
// Lade Team-Struktur
$TreeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', false);
$TreeCalcBot->initStructureUser($user->id, false);
// Hole flache Liste aller Team-Mitglieder
$teamUsersRaw = $this->getTeamUsersFromStructure($TreeCalcBot);
// Sammle User-IDs für Abo-Abfrage
$teamUserIds = [];
foreach ($teamUsersRaw as $teamUser) {
if ($teamUser->user_id && $teamUser->user_id != $user->id) {
$teamUserIds[] = $teamUser->user_id;
}
}
// Hole Abos der Team-Mitglieder
$abos = \App\Models\UserAbo::whereIn('user_id', $teamUserIds)
->where('is_for', 'me')
->with(['user', 'user.account', 'user_abo_items', 'user_abo_items.product'])
->orderBy('next_date', 'asc')
->get();
$data = [
'filter_months' => HTMLHelper::getTransMonths(),
'filter_years' => HTMLHelper::getYearRange(2022),
'abos' => $abos,
];
return view('user.team.abos', $data);
}
/**
* Zeigt die Detail-Ansicht eines Team-Abos an
*/
public function detailAbo($id)
{
$user = User::find(\Auth::user()->id);
$user_abo = \App\Models\UserAbo::findOrFail($id);
// Prüfe ob das Abo zu einem Team-Mitglied gehört
$this->setFilterVars();
$month = session('team_user_filter_month');
$year = session('team_user_filter_year');
$TreeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', false);
$TreeCalcBot->initStructureUser($user->id, false);
$teamUsersRaw = $this->getTeamUsersFromStructure($TreeCalcBot);
$teamUserIds = [];
foreach ($teamUsersRaw as $teamUser) {
if ($teamUser->user_id) {
$teamUserIds[] = $teamUser->user_id;
}
}
// Prüfe Berechtigung
if (!in_array($user_abo->user_id, $teamUserIds)) {
abort(403, 'Unauthorized action. This subscription does not belong to your team.');
}
// Lade Abo-Details (ähnlich wie AboController)
\App\Services\AboOrderCart::initYard($user_abo);
$customer_detail = \App\Services\AboOrderCart::getCustomerDetail();
\App\Services\AboOrderCart::makeOrderYard($user_abo);
$data = [
'user_abo' => $user_abo,
'isAdmin' => false,
'customer_detail' => $customer_detail,
'view' => 'team',
'comp_products' => [],
];
return view('user.team.abo_detail', $data);
}
/**
* Initialisiert die Team-Suche für den eingeloggten User
*/
@ -674,6 +898,9 @@ class TeamController extends Controller
if (!session('team_user_filter_month')) {
session(['team_user_filter_month' => intval(date('m'))]);
}
if (!session('team_user_filter_month_prev')) {
session(['team_user_filter_month_prev' => intval(date('m') - 1)]);
}
if (!session('team_user_filter_year')) {
session(['team_user_filter_year' => intval(date('Y'))]);
}
@ -696,6 +923,9 @@ class TeamController extends Controller
if (Request::get('team_user_filter_month')) {
session(['team_user_filter_month' => Request::get('team_user_filter_month')]);
}
if (Request::get('team_user_filter_month_prev')) {
session(['team_user_filter_month_prev' => Request::get('team_user_filter_month_prev')]);
}
if (Request::get('team_user_filter_year')) {
session(['team_user_filter_year' => Request::get('team_user_filter_year')]);
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Controllers\Api\KasController;
use App\Models\UserShop;
use App\Models\UserShopOnSite;
@ -37,13 +38,14 @@ class UserShopController extends Controller
$user->shop->contact = __('shop.shop_contact_text');
}
$user->shop->accessibility = __('shop.shop_accessibility_text');
}
if ($user->shop && $user->shop->active == 0) {
return redirect(route('user_shop_name_edit'));
}
$data = [
'user' => $user,
];
return view('user.shop', $data);
}
public function translate()
@ -55,7 +57,6 @@ class UserShopController extends Controller
];
return view('user.shop.translate', $data);
}
public function translateStore()
@ -89,10 +90,10 @@ class UserShopController extends Controller
\Session()->flash('alert-save', true);
return redirect(route('user_shop'));
}
private function storeTranslations($user_shop, $lang, $data){
private function storeTranslations($user_shop, $lang, $data)
{
if ($lang == 'de') {
$user_shop->contact = trim(preg_replace('/\s*\n+/', "\n", $data['contact']));
@ -132,12 +133,12 @@ class UserShopController extends Controller
$ret .= $user->email;
return $ret;
}
// Upload FILE -----------------------------------------------------------------------------------------------------------------------
public function uploadImage(){
public function uploadImage()
{
$user = Auth::user();
@ -148,8 +149,7 @@ class UserShopController 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');
@ -184,15 +184,14 @@ class UserShopController extends Controller
}
\Session()->flash('alert-danger', __('msg.file_empty'));
return redirect(route('user_shop'));
}
catch (\Exception $e) {
} catch (\Exception $e) {
\Session()->flash('alert-danger', "Error: " . $e);
return redirect(route('user_shop'));
}
}
public function deleteImage(){
public function deleteImage()
{
$user = Auth::user();
@ -213,14 +212,13 @@ class UserShopController extends Controller
\Session()->flash('alert-success', __('msg.file_deleted'));
return redirect(route('user_shop'));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
return redirect(route('user_shop'));
}
public function uploadOnSiteImage(){
public function uploadOnSiteImage()
{
$user = Auth::user();
$user_shop_id = Request::get('user_shop_id');
@ -232,8 +230,7 @@ class UserShopController 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'];
@ -269,15 +266,14 @@ class UserShopController extends Controller
}
\Session()->flash('alert-danger', __('msg.file_empty'));
return redirect(route('user_shop'));
}
catch (\Exception $e) {
} catch (\Exception $e) {
\Session()->flash('alert-danger', "Error: " . $e);
return redirect(route('user_shop'));
}
}
public function deleteOnSiteImage($image_id, $user_shop_id){
public function deleteOnSiteImage($image_id, $user_shop_id)
{
$user = Auth::user();
if (!$user->shop || $user->shop->id != $user_shop_id) {
@ -293,17 +289,16 @@ class UserShopController extends Controller
\Session()->flash('alert-success', __('msg.file_deleted'));
return redirect(route('user_shop'));
}
\Session()->flash('alert-danger', __('msg.file_not_found'));
return redirect(route('user_shop'));
}
public function userShopRegisterForm(){
public function userShopRegisterForm()
{
if (Request::get('shop_submit') == 'check') {
$rules = array(
@ -359,9 +354,11 @@ class UserShopController extends Controller
}
$user_shop->name = $slug;
$user_shop->slug = $slug;
$user_shop->active = 1;
$user_shop->save();
} else {
$user_shop = UserShop::create([
$user_shop = UserShop::create(
[
'user_id' => $user->id,
'name' => $slug,
'active' => true,
@ -380,11 +377,11 @@ class UserShopController extends Controller
}
return redirect(route('user_shop'));*/
}
}
public function userShopRegisterSubDomain($slug){
public function userShopRegisterSubDomain($slug)
{
$kas = new KasController();
$domain = 'mivita.care';
@ -421,7 +418,8 @@ class UserShopController extends Controller
/**
* @return string to ajax
*/
public function checkUserShopName(){
public function checkUserShopName()
{
$rules = array(
'user_shop_name' => ' required|alpha_dash|unique:user_shops,name|min:4|max:20|full_word_check',
@ -451,7 +449,8 @@ class UserShopController extends Controller
));
}
public function editName(){
public function editName()
{
$user = Auth::user();
$user_shop = $user->shop;
if (!$user_shop) {
@ -465,5 +464,4 @@ class UserShopController extends Controller
];
return view('user.shop_edit_name', $data);
}
}

View file

@ -2,8 +2,9 @@
namespace App\Jobs;
use App\Models\DhlShipment;
use App\Services\DhlApiService;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Services\ShippingService;
use Acme\Dhl\Support\DhlClient;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -73,28 +74,40 @@ class CancelShipmentJob implements ShouldQueue
try {
Log::info('[DHL Queue] Starting shipment cancellation job', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
'attempt' => $this->attempts(),
]);
$dhlService = new DhlApiService();
// Get DHL configuration
$settingController = new \App\Http\Controllers\SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Create DHL client
$dhlClient = new DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Create shipping service
$shippingService = new ShippingService($dhlClient);
// Cancel the shipment
$success = $dhlService->cancelShipment($this->dhlShipment);
$success = $shippingService->cancelLabel($this->dhlShipment->dhl_shipment_no);
if ($success) {
Log::info('[DHL Queue] Shipment cancelled successfully', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
]);
} else {
throw new Exception('Cancellation returned false');
}
} catch (Exception $e) {
Log::error('[DHL Queue] Shipment cancellation failed', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
@ -106,6 +119,11 @@ class CancelShipmentJob implements ShouldQueue
'shipment_id' => $this->dhlShipment->id,
'error' => $e->getMessage(),
]);
// Update shipment status to indicate cancellation failed
$this->dhlShipment->update([
'status' => 'failed',
]);
}
throw $e; // Re-throw to trigger retry mechanism
@ -121,15 +139,22 @@ class CancelShipmentJob implements ShouldQueue
{
Log::error('[DHL Queue] CancelShipmentJob permanently failed', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// You could implement additional failure handling here:
// - Send notification to admin
// - Create manual task for staff to handle cancellation
// - Update shipment status to indicate cancellation failed
// Update shipment status to indicate cancellation failed
try {
$this->dhlShipment->update([
'status' => 'failed',
]);
} catch (Exception $e) {
Log::error('[DHL Queue] Could not update shipment status after failure', [
'shipment_id' => $this->dhlShipment->id,
'error' => $e->getMessage(),
]);
}
}
/**

View file

@ -2,8 +2,10 @@
namespace App\Jobs;
use App\Models\DhlShipment;
use App\Services\DhlApiService;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Services\ReturnsService;
use Acme\Dhl\Support\DhlClient;
use App\Http\Controllers\SettingController;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -73,29 +75,39 @@ class CreateReturnLabelJob implements ShouldQueue
try {
Log::info('[DHL Queue] Starting return label creation job', [
'original_shipment_id' => $this->originalShipment->id,
'original_shipment_number' => $this->originalShipment->shipment_number,
'original_shipment_number' => $this->originalShipment->dhl_shipment_no,
'attempt' => $this->attempts(),
]);
$dhlService = new DhlApiService();
// Get DHL configuration
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
// Create the return label
$returnShipment = $dhlService->createReturnLabel(
$this->originalShipment,
$this->options
// Initialize DHL client
$dhlClient = new DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Use ReturnsService instead of ShippingService
$returnsService = new ReturnsService($dhlClient);
// Prepare return label data from original shipment
$returnData = $this->prepareReturnLabelData($dhlConfig);
Log::info('[DHL Queue] Prepared return data', [
'returnData' => $returnData,
]);
// Create the return label using ReturnsService (with automatic fallback)
$result = $returnsService->createReturn($returnData);
Log::info('[DHL Queue] Return label created successfully', [
'original_shipment_id' => $this->originalShipment->id,
'return_shipment_id' => $returnShipment->id,
'return_shipment_number' => $returnShipment->shipment_number,
'return_shipment_number' => $result['returnNumber'] ?? 'N/A',
]);
// Trigger follow-up actions if specified
if (isset($this->options['auto_track']) && $this->options['auto_track']) {
\App\Jobs\TrackShipmentJob::dispatch($returnShipment)->delay(now()->addMinutes(5));
}
} catch (Exception $e) {
Log::error('[DHL Queue] Return label creation failed', [
'original_shipment_id' => $this->originalShipment->id,
@ -116,6 +128,48 @@ class CreateReturnLabelJob implements ShouldQueue
}
}
/**
* Prepare return label data based on original shipment
*/
private function prepareReturnLabelData(array $dhlConfig): array
{
$order = $this->originalShipment->shoppingOrder;
$recipient = $this->originalShipment->recipient ?? [];
return [
'order_id' => $order->id,
'original_shipment_id' => $this->originalShipment->id,
'weight_kg' => $this->originalShipment->weight_kg,
'label_format' => $this->originalShipment->label_format ?? 'PDF',
// Shipper: Customer sends back to us (swap addresses)
'shipper' => [
'name' => trim(($recipient['firstname'] ?? '') . ' ' . ($recipient['lastname'] ?? '')),
'name2' => $recipient['company'] ?? '',
'street' => $recipient['street'] ?? '',
'houseNumber' => $recipient['houseNumber'] ?? '',
'postalCode' => $recipient['postalCode'] ?? '',
'city' => $recipient['city'] ?? '',
'country' => $recipient['country'] ?? 'DEU',
'email' => $recipient['email'] ?? '',
'phone' => $recipient['phone'] ?? '',
],
// Consignee: Our warehouse (from settings)
'consignee' => [
'name' => $dhlConfig['sender']['company'] ?? 'mivita care gmbh',
'name2' => $dhlConfig['sender']['name'] ?? '',
'street' => $dhlConfig['sender']['street'] ?? 'Leinfeld',
'houseNumber' => $dhlConfig['sender']['house_number'] ?? '2',
'postalCode' => $dhlConfig['sender']['postalCode'] ?? '87755',
'city' => $dhlConfig['sender']['city'] ?? 'Kirchhaslach',
'country' => $dhlConfig['sender']['country'] ?? 'DEU',
'email' => $dhlConfig['sender']['email'] ?? 'versand@mivita.care',
'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789',
],
];
}
/**
* Handle a job failure.
*
@ -125,7 +179,7 @@ class CreateReturnLabelJob implements ShouldQueue
{
Log::error('[DHL Queue] CreateReturnLabelJob permanently failed', [
'original_shipment_id' => $this->originalShipment->id,
'original_shipment_number' => $this->originalShipment->shipment_number,
'original_shipment_number' => $this->originalShipment->dhl_shipment_no,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);

View file

@ -0,0 +1,73 @@
<?php
namespace App\Mail;
use Acme\Dhl\Models\DhlShipment;
use App\Models\ShoppingOrder;
use App\Services\Util;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class MailDhlTracking extends Mailable
{
use Queueable, SerializesModels;
protected $shipments;
protected $order;
public $subject;
/**
* Create a new message instance.
*
* @param DhlShipment|Collection $shipments Single shipment or collection of shipments
* @param ShoppingOrder $order
*/
public function __construct($shipments, ShoppingOrder $order)
{
// Accept both single shipment and collection
if ($shipments instanceof DhlShipment) {
$this->shipments = collect([$shipments]);
} else {
$this->shipments = $shipments;
}
$this->order = $order;
// Adjust subject based on number of shipments
if ($this->shipments->count() > 1) {
$this->subject = __('email.dhl_tracking_subject_multiple', ['count' => $this->shipments->count()]);
} else {
$this->subject = __('email.dhl_tracking_subject');
}
}
public function build()
{
$shoppingUser = $this->order->shopping_user;
$salutation = __('email.hello');
if ($shoppingUser && $shoppingUser->billing_firstname) {
$salutation = __('email.hello') . ' ' . $shoppingUser->billing_firstname;
}
// For backward compatibility, also provide single shipment data
$primaryShipment = $this->shipments->first();
return $this->view('emails.dhl_tracking')->with([
'salutation' => $salutation,
'shipments' => $this->shipments,
'shipment' => $primaryShipment, // Backward compatibility
'order' => $this->order,
'trackingNumber' => $primaryShipment->dhl_shipment_no, // Backward compatibility
'trackingUrl' => $primaryShipment->getTrackingUrl(), // Backward compatibility
'orderNumber' => $this->order->getLastShoppingPayment('reference'),
'greetings' => __('email.greetings'),
'sender' => __('email.sender'),
'url' => Util::getMyMivitaUrl(),
]);
}
}

View file

@ -1,16 +1,11 @@
<?php
namespace App\Mail;
use Storage;
use App\User;
use App\Services\Credit;
use App\Services\Invoice;
use App\Models\UserCredit;
use App\Models\ShoppingOrder;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class MailUserLevelUpdate extends Mailable
{
@ -32,10 +27,13 @@ class MailUserLevelUpdate extends Mailable
{
$title = __('email.update_level_title');
$copy1line = __('email.update_level_copy1line', ['tp' => $this->team_points, 'to' => $this->update_to]);
$copy2line = __('email.update_level_copy2line');
$copy3line = __('email.update_level_copy3line');
return $this->view('emails.blank')->with([
'title' => $title,
'copy1line' => $copy1line,
'content' => $copy2line . '<br><br>' . $copy3line,
]);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserRestoreEmail extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $subject;
/**
* Create a new message instance.
*
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
$this->subject = __('email.user_restore_subject');
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
// Sicherstellen, dass account geladen ist
if (!$this->user->account) {
$this->user->load('account');
}
$firstName = $this->user->account ? $this->user->account->first_name : '';
$title = __('email.user_restore_title');
$greeting = __('email.user_restore_greeting', ['name' => $firstName]);
$copy1line = __('email.user_restore_copy1line');
$copy2line = __('email.user_restore_copy2line');
$copy3line = __('email.user_restore_copy3line');
$payment_account_date = $this->user->getPaymentAccountDateFormat();
$reset_password_url = route('password.request');
return $this->view('emails.user_restore')->with([
'title' => $title,
'greeting' => $greeting,
'copy1line' => $copy1line,
'copy2line' => $copy2line,
'copy3line' => $copy3line,
'payment_account_date' => $payment_account_date,
'reset_password_url' => $reset_password_url,
'user' => $this->user,
]);
}
}

View file

@ -2,8 +2,9 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/**
* App\Models\Category
@ -49,26 +50,21 @@ use Cviebrock\EloquentSluggable\Sluggable;
* @property-read int|null $translations_count
* @mixin \Eloquent
*/
class Category extends Model
{
use Sluggable;
protected $table = 'categories';
protected $fillable = [
'parent_id', 'name', 'headline', 'pos', 'active',
'parent_id',
'name',
'headline',
'pos',
'active',
'slug',
];
public function sluggable() : array
{
return [
'slug' => [
'source' => 'name'
]
];
}
public function parent()
{
return $this->belongsTo('App\Models\Category', 'parent_id');
@ -82,7 +78,6 @@ class Category extends Model
public function product_categories()
{
return $this->hasMany('App\Models\ProductCategory', 'category_id', 'id')->orderBy('pos', 'DESC');
}
public function productCategoriesCountActive()
@ -102,27 +97,30 @@ class Category extends Model
})->orderBy('pos', 'ASC')->count();
}
public function setSlugAttribute($value)
{
$slug = Str::slug($value);
if (self::where('slug', $slug)->exists()) {
$slug = $slug . '-' . self::where('slug', $slug)->count();
}
$this->attributes['slug'] = $slug;
}
public function iq_image()
{
return $this->belongsTo('App\Models\IqImage', 'headline_image_id');
}
public function setPosAttribute($value){
public function setPosAttribute($value)
{
$this->attributes['pos'] = is_numeric($value) ? $value : null;
}
public function translations()
{
return $this->hasMany(TransCategory::class, 'categorie_id');
}
public function getLang($key)
{
$lang = \App::getLocale();
@ -138,6 +136,4 @@ class Category extends Model
$trans = $this->translations->where('language', '=', $lang)->where('key', $key)->first();
return $trans ? $trans->value : '';
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* App\Models\DashboardNews
*
* @property int $id
* @property string|null $title
* @property string|null $teaser
* @property string|null $content
* @property array|null $trans_title
* @property array|null $trans_teaser
* @property array|null $trans_content
* @property bool $active
* @property \Illuminate\Support\Carbon|null $display_date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class DashboardNews extends Model
{
protected $table = 'dashboard_news';
protected $casts = [
'trans_title' => 'array',
'trans_teaser' => 'array',
'trans_content' => 'array',
'file_links' => 'array',
'active' => 'boolean',
'display_date' => 'date',
];
protected $fillable = [
'title',
'teaser',
'content',
'trans_title',
'trans_teaser',
'trans_content',
'file_links',
'active',
'display_date',
];
/**
* Get translated value or fallback to default (German)
*/
public function getLang($key)
{
$lang = \App::getLocale();
if ($lang == 'de') {
return $this->{$key};
}
$trans = $this->getTrans($key, $lang);
if (!$trans || $trans == '') {
return $this->{$key};
}
return $trans;
}
/**
* Get specific translation
*/
public function getTrans($key, $lang)
{
$transKey = 'trans_' . $key;
if (!empty($this->{$transKey}[$lang])) {
return $this->{$transKey}[$lang];
}
return "";
}
/**
* Get active news
*/
public static function getActiveNews()
{
return self::where('active', true)
->orderBy('created_at', 'DESC')
->first();
}
/**
* Get formatted display date or created_at as fallback
*/
public function getDisplayDateFormatted()
{
$date = $this->display_date ?: $this->created_at;
return $date ? $date->format('d.m.Y') : '';
}
/**
* Get file links for current language
*/
public function getFileLinks($lang = null)
{
if (!$lang) {
$lang = \App::getLocale();
}
if (!$this->file_links || !isset($this->file_links[$lang])) {
return [];
}
return collect($this->file_links[$lang])->map(function ($link) {
if (isset($link['file_id'])) {
$file = \App\Models\DcFile::find($link['file_id']);
if ($file && $file->active) {
return [
'file' => $file,
'label' => $link['label'] ?? $file->original_name,
];
}
}
return null;
})->filter()->values();
}
/**
* Check if news has file links for current language
*/
public function hasFileLinks($lang = null)
{
return count($this->getFileLinks($lang)) > 0;
}
}

View file

@ -6,6 +6,7 @@
namespace App\Models;
use App\Services\Util;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
@ -29,7 +30,7 @@ use Illuminate\Database\Eloquent\Model;
* @property Homeparty $homeparty
* @property HomepartyUser $homeparty_user
* @property Product $product
* @package App\Models
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem query()
@ -47,8 +48,11 @@ use Illuminate\Database\Eloquent\Model;
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem whereTaxRate($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\HomepartyUserOrderItem whereUpdatedAt($value)
*
* @property float|null $ek_price_net
*
* @method static \Illuminate\Database\Eloquent\Builder|HomepartyUserOrderItem whereEkPriceNet($value)
*
* @mixin \Eloquent
*/
class HomepartyUserOrderItem extends Model
@ -63,10 +67,10 @@ class HomepartyUserOrderItem extends Model
'price' => 'float',
'price_net' => 'float',
'tax_rate' => 'float',
'points' => 'int',
'points' => 'float',
'margin' => 'float',
'ek_price' => 'float',
'ek_price_net' => 'float'
'ek_price_net' => 'float',
];
@ -82,7 +86,7 @@ class HomepartyUserOrderItem extends Model
'margin',
'ek_price',
'ek_price_net',
'slug'
'slug',
];
public function homeparty()
@ -100,6 +104,11 @@ class HomepartyUserOrderItem extends Model
return $this->belongsTo(Product::class);
}
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function getFormattedPrice()
{
return formatNumber($this->attributes['price']);
@ -152,7 +161,7 @@ class HomepartyUserOrderItem extends Model
public function getFormattedTotalPoints()
{
return formatNumber($this->attributes['points'] * $this->attributes['qty'], 0);
return formatNumber($this->attributes['points'] * $this->attributes['qty'], 2);
}
public function getTotalPrice()
@ -160,10 +169,9 @@ class HomepartyUserOrderItem extends Model
return (float) ($this->attributes['price'] * $this->attributes['qty']);
}
public function getTotalPoints()
{
return ($this->attributes['points'] * $this->attributes['qty']);
return $this->attributes['points'] * $this->attributes['qty'];
}
public function geTotalPriceNet()
@ -191,7 +199,6 @@ class HomepartyUserOrderItem extends Model
return (float) (($this->attributes['price'] - $this->attributes['ek_price']) * $this->attributes['qty']);
}
public function getCurrencyByKey($key)
{
$rNumber = 0;
@ -208,11 +215,9 @@ class HomepartyUserOrderItem extends Model
case 'TotalEKPrice':
$rNumber = $this->geTotalEKPrice() * $faktor;
break;
}
}
return formatNumber($rNumber);
}
}

View file

@ -40,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProductCategory[] $categories
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProductImage[] $images
* @property-write mixed $price_vk
*
* @method static bool|null forceDelete()
* @method static \Illuminate\Database\Query\Builder|\App\Models\Product onlyTrashed()
* @method static bool|null restore()
@ -71,39 +72,56 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereUsage($value)
* @method static \Illuminate\Database\Query\Builder|\App\Models\Product withTrashed()
* @method static \Illuminate\Database\Query\Builder|\App\Models\Product withoutTrashed()
*
* @property string|null $slug
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product findSimilarSlugs($attribute, $config, $slug)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product query()
*
* @property int|null $weight
* @property int|null $show_at
* @property array|null $action
* @property-read int|null $attributes_count
* @property-read int|null $categories_count
* @property-read int|null $images_count
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereAction($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereShowAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereWeight($value)
*
* @property int|null $points
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProductImage[] $imagesActive
* @property-read int|null $images_active_count
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product wherePoints($value)
*
* @property string|null $identifier
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereIdentifier($value)
*
* @property int|null $upgrade_to_id
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereUpgradeToId($value)
*
* @property int|null $contents_total
* @property int|null $unit
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereContentsTotal($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereUnit($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\CountryPrice[] $country_prices
* @property-read int|null $country_prices_count
* @property int|null $wp_number
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereWpNumber($value)
*
* @property bool|null $shipping_addon
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Product whereShippingAddon($value)
*
* @property-read int|null $ingredients_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProductIngredient[] $product_ingredients
* @property-read int|null $product_ingredients_count
@ -111,35 +129,43 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property array|null $show_on
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Ingredient[] $p_ingredients
* @property-read int|null $p_ingredients_count
*
* @method static \Illuminate\Database\Eloquent\Builder|Product whereNoCommission($value)
* @method static \Illuminate\Database\Eloquent\Builder|Product whereShowOn($value)
* @method static \Illuminate\Database\Eloquent\Builder|Product withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
*
* @property string|null $ean
*
* @method static \Illuminate\Database\Eloquent\Builder|Product whereEan($value)
*
* @property bool|null $no_free_shipping
* @property bool|null $buying_restriction
* @property int|null $buying_restriction_amount
*
* @method static \Illuminate\Database\Eloquent\Builder|Product whereBuyingRestriction($value)
* @method static \Illuminate\Database\Eloquent\Builder|Product whereBuyingRestrictionAmount($value)
* @method static \Illuminate\Database\Eloquent\Builder|Product whereNoFreeShipping($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductBuying> $product_buyings
* @property-read int|null $product_buyings_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductBuying> $product_buyings
* @property bool|null $sponsor_buying_points
* @property int|null $sponsor_buying_points_amount
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductBuying> $product_buyings
*
* @method static \Illuminate\Database\Eloquent\Builder|Product whereSponsorBuyingPoints($value)
* @method static \Illuminate\Database\Eloquent\Builder|Product whereSponsorBuyingPointsAmount($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductBuying> $product_buyings
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TransProduct> $translations
* @property-read int|null $translations_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductCategory> $product_categories
* @property-read int|null $product_categories_count
*
* @mixin \Eloquent
*/
class Product extends Model
{
/*identifiers
show_upgrade # in membership payment can upgrade this package to show order
show_order # in membership payment show always
@ -167,11 +193,9 @@ class Product extends Model
'buying_restriction_amount' => 'int',
'sponsor_buying_points' => 'bool',
'sponsor_buying_points_amount' => 'int',
];
use Sluggable;
use Sluggable;
use SoftDeletes;
protected $dates = ['deleted_at'];
@ -210,7 +234,7 @@ class Product extends Model
'sponsor_buying_points_amount',
'identifier',
'action',
'upgrade_to_id'
'upgrade_to_id',
];
public $identifiers_types = [
@ -221,6 +245,7 @@ class Product extends Model
'upgrade_member' => 'Berater upgrade zur Karriere ID',
'proportional_voucher' => 'Anteiliger Gutschein Berater',
];
public $unitTypes = [
0 => '',
1 => 'ml',
@ -280,47 +305,57 @@ class Product extends Model
* @return array Configuration for slug generation.
*/
/****** e935bd41-f49b-4736-9603-2da86dc27f25 *******/ public function sluggable() : array
/****** e935bd41-f49b-4736-9603-2da86dc27f25 *******/
public function sluggable(): array
{
return [
'slug' => [
'source' => 'name'
]
'source' => 'name',
],
];
}
public function product_buyings(){
public function product_buyings()
{
return $this->hasMany('App\Models\ProductBuying', 'product_id', 'id');
}
public function attributes(){
public function attributes()
{
return $this->hasMany('App\Models\ProductAttribute', 'product_id', 'id');
}
public function categories(){
public function categories()
{
return $this->hasMany('App\Models\ProductCategory', 'product_id', 'id')->orderBy('pos', 'DESC');
}
public function product_categories(){
public function product_categories()
{
return $this->hasMany('App\Models\ProductCategory', 'product_id', 'id')->orderBy('pos', 'DESC');
}
public function images(){
public function images()
{
return $this->hasMany('App\Models\ProductImage', 'product_id', 'id')->orderBy('pos');
}
public function imagesActive(){
public function imagesActive()
{
return $this->hasMany('App\Models\ProductImage', 'product_id', 'id')->where('active', true)->orderBy('pos');
}
public function getImageUrl(){
public function getImageUrl()
{
if (count($this->imagesActive)) {
return route('product_image', [$this->imagesActive->first()->slug]);
}
return "";
return '';
}
public function getProductUrl(){
public function getProductUrl()
{
return 'https://mivita.shop/produkte/alle-produkte/'.$this->slug;
}
@ -346,14 +381,52 @@ class Product extends Model
return $this->hasMany(ProductIngredient::class, 'product_ingredients', 'id');
}
public function getActionName($id = 0){
/**
* Bundle-Items: Enthaltene Produkte in diesem Set/Kit
*/
public function bundleItems()
{
return $this->belongsToMany(Product::class, 'product_bundles', 'product_id', 'bundle_product_id')
->withPivot('quantity', 'pos')
->orderBy('product_bundles.pos');
}
/**
* Bundle-Parents: Sets/Kits, die dieses Produkt enthalten
*/
public function bundleParents()
{
return $this->belongsToMany(Product::class, 'product_bundles', 'bundle_product_id', 'product_id')
->withPivot('quantity', 'pos');
}
/**
* Prüft ob dieses Produkt ein Set/Kit ist (enthält andere Produkte)
*/
public function isBundle(): bool
{
return $this->bundleItems()->count() > 0;
}
/**
* Holt Bundle-Items mit eager loading
*/
public function product_bundles()
{
return $this->hasMany(ProductBundle::class, 'product_id', 'id')->orderBy('pos');
}
public function getActionName($id = 0)
{
if (isset($this->actions[$id])) {
return $this->actions[$id];
}
return false;
}
public function getUpgradeToIdName($from){
public function getUpgradeToIdName($from)
{
if ($from === 'payment_for_shop_upgrade') {
$value = Product::find($this->upgrade_to_id);
}
@ -363,47 +436,67 @@ class Product extends Model
if ($value) {
return $value->name;
}
return "not found";
return 'not found';
}
public function _format_number($value){
return preg_replace("/[^0-9,]/", "", $value);
public function _format_number($value)
{
return preg_replace('/[^0-9,]/', '', $value);
}
public function setPriceAttribute( $value ) {
public function setPriceAttribute($value)
{
$this->attributes['price'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function setPriceEkAttribute( $value ) {
public function setPriceEkAttribute($value)
{
$this->attributes['price_ek'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function setTaxAttribute( $value ) {
public function setTaxAttribute($value)
{
$this->attributes['tax'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function setPriceOldAttribute( $value ) {
public function setPriceOldAttribute($value)
{
$this->attributes['price_old'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? Util::reFormatNumber($value) : null;
}
public function getFormattedPoints()
{
return isset($this->attributes['points']) ? Util::formatNumber($this->attributes['points']) : '';
}
public function getFormattedPrice()
{
return isset($this->attributes['price']) ? Util::formatNumber($this->attributes['price']) : "";
return isset($this->attributes['price']) ? Util::formatNumber($this->attributes['price']) : '';
}
public function getFormattedPriceEk()
{
return isset($this->attributes['price_ek']) ? Util::formatNumber($this->attributes['price_ek']) : "";
return isset($this->attributes['price_ek']) ? Util::formatNumber($this->attributes['price_ek']) : '';
}
public function getFormattedPriceOld()
{
return isset($this->attributes['price_old']) ? Util::formatNumber($this->attributes['price_old']) : "";
return isset($this->attributes['price_old']) ? Util::formatNumber($this->attributes['price_old']) : '';
}
/* price by user Factor */
private function calcPriceUserFactor($price, $user=null){
private function calcPriceUserFactor($price, $user = null)
{
if ($this->no_commission) {
return $price;
}
@ -412,10 +505,12 @@ class Product extends Model
$margin = (($user->user_level->margin - 100) * -1) / 100;
$price = $price * $margin;
}
return $price;
}
private function calcPriceUserCommission($price, $user){
private function calcPriceUserCommission($price, $user)
{
if ($this->no_commission) {
return $price;
}
@ -424,18 +519,22 @@ class Product extends Model
$margin = $user->user_level->margin;
$price = $price / 100 * $margin;
}
return $price;
}
/* price net */
private function calcPriceNet($price, $country=null){
private function calcPriceNet($price, $country = null)
{
$tax = $this->getTaxWith($country);
$tax_rate = ($tax + 100) / 100;
return $price / $tax_rate;
}
// price calu with
public function getPriceWith(Bool $net = true, Bool $ufactor = true, $country = null, $commission=false, $user = null){
public function getPriceWith(bool $net = true, bool $ufactor = true, $country = null, $commission = false, $user = null)
{
$price = isset($this->attributes['price']) ? $this->attributes['price'] : null;
$cprice = $country ? $this->getCPrice($country) : null;
$price = $cprice ? $cprice : $price;
@ -447,18 +546,20 @@ class Product extends Model
}
/* out */
public function getFormattedPriceWith(Bool $net = true, Bool $ufactor = true, $country = null, $commission=false)
public function getFormattedPriceWith(bool $net = true, bool $ufactor = true, $country = null, $commission = false)
{
return isset($this->attributes['price']) ? Util::formatNumber($this->getPriceWith($net, $ufactor, $country, $commission)) : "";
return isset($this->attributes['price']) ? Util::formatNumber($this->getPriceWith($net, $ufactor, $country, $commission)) : '';
}
public function getTaxWith($country = null){
public function getTaxWith($country = null)
{
$tax = isset($this->attributes['tax']) ? $this->attributes['tax'] : null;
$ctax = $country ? $this->getCTax($country) : null;
return $ctax !== null ? $ctax : $tax;
}
public function getFormattedPriceOldWith(Bool $net = true, Bool $ufactor = true, $country = null)
public function getFormattedPriceOldWith(bool $net = true, bool $ufactor = true, $country = null)
{
$price = isset($this->attributes['price_old']) ? $this->attributes['price_old'] : null;
$cprice = $country ? $this->getCPriceOld($country) : null;
@ -466,16 +567,17 @@ class Product extends Model
$price = $net ? $this->calcPriceNet($price, $country) : $price;
$price = $ufactor ? $this->calcPriceUserFactor($price) : $price;
$price = round($price, 2);
return isset($price) ? Util::formatNumber($price) : "";
}
return isset($price) ? Util::formatNumber($price) : '';
}
public function getFormattedTax($country = null)
{
return isset($this->attributes['tax']) ? Util::formatNumber($this->getTaxWith($country), 0) : "";
return isset($this->attributes['tax']) ? Util::formatNumber($this->getTaxWith($country), 0) : '';
}
public function getBasePriceFormattedFullWith(Bool $net = true, Bool $ufactor = true, $country = null){
public function getBasePriceFormattedFullWith(bool $net = true, bool $ufactor = true, $country = null)
{
$price = $this->getPriceWith($net, $ufactor, $country);
if ($price = $this->getBasePriceWith($price)) {
$unit = $this->attributes['unit'];
@ -488,10 +590,12 @@ class Product extends Model
return Util::formatNumber($price).' € / 1 '.$this->getUnitType();
}
}
return "";
return '';
}
public function getBasePriceWith($price){
public function getBasePriceWith($price)
{
if (isset($this->attributes['unit']) && isset($this->attributes['contents_total']) && $this->attributes['contents_total'] != 0) {
$unit = $this->attributes['unit'];
// ml g
@ -503,10 +607,12 @@ class Product extends Model
return $price * 1000 / $this->attributes['contents_total'];
}
}
return "";
return '';
}
public function getBasePriceFormattedFull(){
public function getBasePriceFormattedFull()
{
if ($price = $this->getBasePrice()) {
$unit = $this->attributes['unit'];
// ml g
@ -518,17 +624,21 @@ class Product extends Model
return Util::formatNumber($price).' € / 1 '.$this->getUnitType();
}
}
return "";
return '';
}
public function getBasePriceFormatted(){
public function getBasePriceFormatted()
{
if ($price = $this->getBasePrice()) {
return Util::formatNumber($price);
}
return "";
return '';
}
public function getBasePrice(){
public function getBasePrice()
{
if (isset($this->attributes['unit']) && isset($this->attributes['contents_total']) && $this->attributes['contents_total'] != 0) {
$unit = $this->attributes['unit'];
// ml g
@ -540,28 +650,34 @@ class Product extends Model
return $this->attributes['price'] * 1000 / $this->attributes['contents_total'];
}
}
return "";
return '';
}
public function getUnitType(){
public function getUnitType()
{
return isset($this->unitTypes[$this->unit]) ? __($this->unitTypes[$this->unit]) : '-';
}
public function getShowAtType(){
public function getShowAtType()
{
return isset($this->showATs[$this->show_at]) ? $this->showATs[$this->show_at] : '-';
}
public function getShowOnTypes(){
public function getShowOnTypes()
{
$ret = [];
if ($this->show_on && is_array($this->show_on)) {
foreach ($this->show_on as $show) {
$ret[] = isset($this->showONs[$show]) ? $this->showONs[$show] : '-';
}
}
return $ret;
}
public function setPosAttribute($value){
public function setPosAttribute($value)
{
$this->attributes['pos'] = is_numeric($value) ? $value : null;
}
@ -572,58 +688,75 @@ class Product extends Model
return $this->{$key};
}
$trans = $this->getTrans($key, $lang);
return $trans != '' ? $trans : $this->{$key};
}
public function getTrans($key, $lang)
{
$trans = $this->translations->where('language', '=', $lang)->where('key', $key)->first();
return $trans ? $trans->value : '';
}
public function getTranNames()
{
$ret = "";
$ret = '';
foreach ((array) $this->trans_name as $value) {
$ret .= $value.', ';
}
return rtrim($ret, ', ');
}
public function getCountryPrice(Country $country){
public function getCountryPrice(Country $country)
{
if ($country->own_eur) {
return $this->country_prices->where('country_id', '=', $country->id)->first() ?: new CountryPrice();
}
return new CountryPrice();
return $this->country_prices->where('country_id', '=', $country->id)->first() ?: new CountryPrice;
}
public function getCPrice(Country $country){
return new CountryPrice;
}
public function getCPrice(Country $country)
{
return $this->getCountryPrice($country)->c_price;
}
public function getCTax(Country $country){
public function getCTax(Country $country)
{
return $this->getCountryPrice($country)->c_tax;
}
public function getCPriceOld(Country $country){
public function getCPriceOld(Country $country)
{
return $this->getCountryPrice($country)->c_price_old;
}
public function getCCurrency(Country $country){
public function getCCurrency(Country $country)
{
return $this->getCountryPrice($country)->c_currency;
}
public function getRealPrice(Country $country){
public function getRealPrice(Country $country)
{
if ($country->own_eur && $this->getCPrice($country)) {
return $this->getCPrice($country);
}
return $this->price;
}
public function getFormattedPriceCurrencyWith(Bool $net = true, Bool $ufactor = true, Country $country = null, $commission = false){
$ret = "";
public function getFormattedPriceCurrencyWith(bool $net = true, bool $ufactor = true, ?Country $country = null, $commission = false)
{
$ret = '';
if ($country && isset($country->currency) && $country->currency) {
$price = $this->getPriceWith($net, $ufactor, $country, $commission);
$ret = formatNumber($price * $country->currency_faktor)." ".$country->currency_unit;
$ret = formatNumber($price * $country->currency_faktor).' '.$country->currency_unit;
return '<br><span class="small">~'.$ret.'<span>';
}
return "" ;
return '';
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* ProductBundle Model - Verwaltet Set/Kit Produkt-Beziehungen
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class ProductBundle
*
* @property int $id
* @property int $product_id
* @property int $bundle_product_id
* @property int $quantity
* @property int $pos
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Product $product
* @property Product $bundleProduct
*
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle query()
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereProductId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereBundleProductId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereQuantity($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle wherePos($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProductBundle whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class ProductBundle extends Model
{
protected $table = 'product_bundles';
protected $casts = [
'product_id' => 'int',
'bundle_product_id' => 'int',
'quantity' => 'int',
'pos' => 'int',
];
protected $fillable = [
'product_id',
'bundle_product_id',
'quantity',
'pos',
];
/**
* Das Set/Kit-Produkt (Parent)
*/
public function product()
{
return $this->belongsTo(Product::class, 'product_id');
}
/**
* Das enthaltene Produkt (Child)
*/
public function bundleProduct()
{
return $this->belongsTo(Product::class, 'bundle_product_id');
}
}

View file

@ -6,10 +6,10 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Model;
/**
* Class ShoppingCollectOrder
@ -33,7 +33,7 @@ use App\User;
* @property Carbon|null $updated_at
* @property ShoppingOrder|null $shopping_order
* @property User $user
* @package App\Models
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder query()
@ -54,8 +54,11 @@ use App\User;
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder whereTaxTotal($value)
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder whereUserId($value)
*
* @property array|null $net_split
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingCollectOrder whereNetSplit($value)
*
* @mixin \Eloquent
*/
class ShoppingCollectOrder extends Model
@ -72,7 +75,7 @@ class ShoppingCollectOrder extends Model
'price_total' => 'float',
'tax_total' => 'float',
'qty_total' => 'int',
'points' => 'int',
'points' => 'float',
'status' => 'int',
'tax_split' => 'array',
'net_split' => 'array',
@ -96,7 +99,7 @@ class ShoppingCollectOrder extends Model
'net_split',
'orders',
'shop_items',
'status'
'status',
];
public static $statusTypes = [
@ -115,6 +118,16 @@ class ShoppingCollectOrder extends Model
return $this->belongsTo(User::class);
}
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function getFormattedPoints()
{
return isset($this->attributes['points']) ? formatNumber($this->attributes['points']) : '';
}
public function addTaxToSplit($tax_rate, $add_tax)
{
$tax_split = $this->tax_split;
@ -172,23 +185,24 @@ class ShoppingCollectOrder extends Model
'user_price_net',
'user_price_total_net',
'user_tax',
'user_tax_total'
'user_tax_total',
];
$intFields = [
// Points werden jetzt auch als Dezimalzahlen behandelt
$decimalFields = [
'points_total',
'points'
'points',
];
foreach ($numberFields as $field) {
if (isset($shop_item->$field)) {
$shop_item->$field = number_format($shop_item->$field, 2);
$shop_item->$field = number_format($shop_item->$field, 2, '.', '');
}
}
foreach ($intFields as $field) {
foreach ($decimalFields as $field) {
if (isset($shop_item->$field)) {
$shop_item->$field = intval($shop_item->$field);
$shop_item->$field = round((float) $shop_item->$field, 2);
}
}
@ -203,6 +217,7 @@ class ShoppingCollectOrder extends Model
public function initShoppingOrder($order)
{
$order['shopping_order'] = ShoppingOrder::findOrFail($order['order_id']);
return $order;
}
}

View file

@ -154,6 +154,7 @@ class ShoppingOrder extends Model
'net_split' => 'array',
'abo_interval' => 'int',
'is_abo' => 'bool',
'points' => 'float',
];
public static $shippedTypes = [
@ -421,6 +422,16 @@ class ShoppingOrder extends Model
return formatNumber($this->attributes['total_shipping']);
}
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function getFormattedPoints()
{
return isset($this->attributes['points']) ? formatNumber($this->attributes['points']) : "";
}
public function getPriceVkNetBy($product_id)
{
if ($product = Product::find($product_id)) {

View file

@ -91,7 +91,7 @@ class ShoppingOrderItem extends Model
'tax' => 'float',
'price_vk_net' => 'float',
'discount' => 'float',
'points' => 'int',
'points' => 'float',
];
public function shopping_order()
@ -149,4 +149,14 @@ class ShoppingOrderItem extends Model
{
return formatNumber($this->attributes['price_net'] * $this->attributes['qty']);
}
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function getFormattedPoints()
{
return isset($this->attributes['points']) ? formatNumber($this->attributes['points']) : "";
}
}

View file

@ -40,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read int|null $shopping_orders_count
* @property-read \App\Models\Country $billing_country
* @property-read \App\Models\Country $shipping_country
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser query()
@ -70,57 +71,80 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereShippingSalutation($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereShippingZipcode($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereUpdatedAt($value)
*
* @property int|null $orders
* @property-read \App\Models\ShoppingOrder $shopping_order
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingOrder[] $shopping_orders
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereOrders($value)
*
* @property int|null $abo_options
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereAboOptions($value)
*
* @property int|null $member_id
* @property int|null $number
* @property bool $is_like
* @property array|null $notice
* @property-read \App\User|null $auth_user
* @property-read \App\User|null $member
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereIsLike($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereMemberId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereNotice($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereNumber($value)
*
* @property bool|null $has_buyed
* @property bool|null $subscribed
* @property int|null $wp_order_number
* @property string|null $wp_order_date
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereHasBuyed($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereSubscribed($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereWpOrderDate($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereWpOrderNumber($value)
*
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property string|null $user_deleted_at
*
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingUser onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereUserDeletedAt($value)
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingUser withTrashed()
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingUser withoutTrashed()
*
* @property string|null $mode
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereMode($value)
*
* @property bool|null $faker_mail
* @property string|null $shipping_email
* @property string|null $is_for
* @property string|null $is_from
* @property int|null $shopping_user_id
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereFakerMail($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereIsFor($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereIsFrom($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereShippingEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereShoppingUserId($value)
*
* @property int|null $homeparty_id
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingUser whereHomepartyId($value)
*
* @property int|null $shopping_collect_order_id
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingUser whereShoppingCollectOrderId($value)
*
* @property string|null $remarks
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingUser whereRemarks($value)
*
* @property string|null $language
*
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingUser whereLanguage($value)
*
* @mixin \Eloquent
*/
class ShoppingUser extends Model
@ -128,10 +152,9 @@ class ShoppingUser extends Model
protected $table = 'shopping_users';
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $fillable = [
'auth_user_id',
'member_id',
@ -163,6 +186,7 @@ class ShoppingUser extends Model
'shipping_city',
'shipping_country_id',
'shipping_phone',
'shipping_postnumber',
'has_buyed',
'subscribed',
'notice',
@ -188,17 +212,17 @@ class ShoppingUser extends Model
'wp_order_number' => 'int',
];
// can null
public function member()
{
return $this->belongsTo('App\User', 'member_id');
}
public function auth_user()
{
return $this->belongsTo('App\User', 'auth_user_id');
}
public function billing_country()
{
return $this->belongsTo('App\Models\Country', 'billing_country_id');
@ -219,22 +243,26 @@ class ShoppingUser extends Model
return $this->hasOne('App\Models\ShoppingOrder', 'shopping_user_id');
}
public function getLocale(){
public function getLocale()
{
return $this->language ? $this->language : \App::getLocale();
}
public function setNotice($key, $value){
public function setNotice($key, $value)
{
$notice = $this->notice;
$notice[$key] = $value;
$this->notice = $notice;
$this->save();
}
public function getNotice($key){
public function getNotice($key)
{
return isset($this->notice[$key]) ? $this->notice[$key] : false;
}
public function removeNotice($key){
public function removeNotice($key)
{
$notice = $this->notice;
if (isset($notice[$key])) {
unset($notice[$key]);
@ -243,26 +271,32 @@ class ShoppingUser extends Model
$this->save();
}
public function firstEntryByNumber(){
public function firstEntryByNumber()
{
if ($this->number > 0) {
if ($shopping_user = ShoppingUser::where('number', $this->number)->orderBy('created_at', 'ASC')->first()) {
return $shopping_user;
}
}
return $this;
}
public function lastEntryByNumber(){
public function lastEntryByNumber()
{
if ($this->number > 0) {
if ($shopping_user = ShoppingUser::where('number', $this->number)->orderBy('created_at', 'DESC')->first()) {
return $shopping_user;
}
}
return $this;
}
public function getOrderPaymentFor() {
public function getOrderPaymentFor()
{
switch ($this->is_from) {
case 'wizard':
@ -280,30 +314,36 @@ class ShoppingUser extends Model
case 'collection':
return 8;
}
return 0;
}
public function setIsForAttribute($value){
public function setIsForAttribute($value)
{
if ($value === 'abo-me' || $value === 'me') {
$this->attributes['is_for'] = 'me';
return;
}
if ($value === 'ot-member' || $value === 'ot-customer' || $value === 'abo-ot-member' || $value === 'abo-ot-customer') {
$this->attributes['is_for'] = 'ot';
return;
}
$this->attributes['is_for'] = $value;
}
public function getAPIShippedType() {
public function getAPIShippedType()
{
if ($this->shopping_order) {
return $this->shopping_order->getAPIShippedType();
}
return "free";
return 'free';
}
public function getFullNameAsArray() {
public function getFullNameAsArray()
{
return [
'company' => $this->billing_company,
'salutation' => $this->billing_salutation,
@ -314,16 +354,19 @@ class ShoppingUser extends Model
];
}
public function getAllOrdersByMember(){
public function getAllOrdersByMember()
{
return ShoppingUserService::getAllOrdersByMember($this);
}
public function getDeliveryCountry($get_country = false){
public function getDeliveryCountry($get_country = false)
{
if ($this->same_as_billing == 1) {
if ($this->billing_country_id) {
if ($get_country) {
return $this->billing_country;
}
return $this->billing_country->getLocated();
}
} else {
@ -331,9 +374,55 @@ class ShoppingUser extends Model
if ($get_country) {
return $this->shipping_country;
}
return $this->shipping_country->getLocated();
}
}
return 'not set';
}
/**
* Prüft ob es sich um eine Packstation/Paketbox-Lieferung handelt
*/
public function isPackstationDelivery(): bool
{
return ! empty($this->shipping_postnumber);
}
/**
* Liefert die effektive Lieferadresse (berücksichtigt same_as_billing)
*/
public function getEffectiveShippingAddress(): array
{
if ($this->same_as_billing) {
return [
'salutation' => $this->billing_salutation,
'company' => $this->billing_company,
'firstname' => $this->billing_firstname,
'lastname' => $this->billing_lastname,
'address' => $this->billing_address,
'address_2' => $this->billing_address_2,
'zipcode' => $this->billing_zipcode,
'city' => $this->billing_city,
'country_id' => $this->billing_country_id,
'phone' => $this->billing_phone,
'postnumber' => null, // Bei same_as_billing keine Packstation
];
}
return [
'salutation' => $this->shipping_salutation,
'company' => $this->shipping_company,
'firstname' => $this->shipping_firstname,
'lastname' => $this->shipping_lastname,
'address' => $this->shipping_address,
'address_2' => $this->shipping_address_2,
'zipcode' => $this->shipping_zipcode,
'city' => $this->shipping_city,
'country_id' => $this->shipping_country_id,
'phone' => $this->shipping_phone,
'postnumber' => $this->shipping_postnumber,
];
}
}

View file

@ -173,12 +173,20 @@ class UserAbo extends Model
return $this->hasMany(UserAboItem::class);
}
public function getCountOrders(){
public function getCountOrders()
{
//sind bezahlte Bestellungen
return $this->user_abo_orders->where('status', '>=', 2)->count();
}
public function setStartDateAttribute( $value ) {
public function getCountPaidOrders()
{
//sind bezahlte Bestellungen
return $this->user_abo_orders->where('status', '>=', 2)->where('paid', true)->count();
}
public function setStartDateAttribute($value)
{
$this->attributes['start_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getStartDateAttribute()
@ -186,7 +194,8 @@ class UserAbo extends Model
return $this->attributes['start_date'] ? Carbon::parse($this->attributes['start_date'])->format(\Util::formatDateDB()) : '';
}
public function setLastDateAttribute( $value ) {
public function setLastDateAttribute($value)
{
$this->attributes['last_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getLastDateAttribute()
@ -194,7 +203,8 @@ class UserAbo extends Model
return $this->attributes['last_date'] ? Carbon::parse($this->attributes['last_date'])->format(\Util::formatDateDB()) : '';
}
public function setNextDateAttribute( $value ) {
public function setNextDateAttribute($value)
{
$this->attributes['next_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getNextDateAttribute()
@ -202,7 +212,8 @@ class UserAbo extends Model
return $this->attributes['next_date'] ? Carbon::parse($this->attributes['next_date'])->format(\Util::formatDateDB()) : '';
}
public function setCancelDateAttribute( $value ) {
public function setCancelDateAttribute($value)
{
$this->attributes['cancel_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getCancelDateAttribute()
@ -219,20 +230,23 @@ class UserAbo extends Model
return $this->attributes['is_for'] === 'me' ? '<span class="badge badge-outline-warning-dark">' . __('tables.adviser') . '</span>' : '<span class="badge badge-outline-info">' . __('tables.customer') . '</span>';
}
public function getStatusFormated(){
public function getStatusFormated()
{
return '<span class="badge badge-pill badge-' . $this->getStatusColor() . '">' . $this->getStatusType() . '</span>';
}
public function getStatusType(){
public function getStatusType()
{
return isset(self::$statusTypes[$this->status]) ? __('abo.' . self::$statusTypes[$this->status]) : "";
}
public function getStatusColor(){
public function getStatusColor()
{
return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : "default";
}
public function getPaymentType(){
public function getPaymentType()
{
return $this->clearingtype === 'wlt' ? __('payment.paypal') : __('payment.credit_card');
}
}

View file

@ -40,14 +40,16 @@ class UserAboOrder extends Model
protected $casts = [
'user_abo_id' => 'int',
'shopping_order_id' => 'int',
'status' => 'int'
'status' => 'int',
'paid' => 'bool'
];
protected $fillable = [
'user_abo_id',
'user_id',
'shopping_order_id',
'status'
'status',
'paid'
];
public static $statusTypes = [
@ -84,16 +86,18 @@ class UserAboOrder extends Model
return $this->belongsTo(UserAbo::class);
}
public function getStatusFormated(){
public function getStatusFormated()
{
return '<span class="badge badge-pill badge-' . $this->getStatusColor() . '">' . $this->getStatusType() . '</span>';
}
public function getStatusType(){
public function getStatusType()
{
return isset(self::$statusTypes[$this->status]) ? __('abo.' . self::$statusTypes[$this->status]) : "";
}
public function getStatusColor(){
public function getStatusColor()
{
return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : "default";
}
}

View file

@ -136,10 +136,49 @@ class UserAccount extends Model
{
protected $table = 'user_accounts';
protected $fillable = [
'm_account', 'm_salutation', 'm_first_name', 'm_last_name', 'm_notes', 'company', 'salutation', 'first_name', 'last_name', 'address', 'address_2', 'zipcode', 'city', 'country_id', 'pre_phone_id', 'phone', 'pre_mobil_id', 'mobil',
'tax_number', 'tax_identification_number', 'taxable_sales', 'same_as_billing',
'shipping_salutation', 'shipping_company', 'shipping_firstname', 'shipping_lastname', 'shipping_address', 'shipping_address_2', 'shipping_zipcode', 'shipping_city', 'shipping_country_id', 'shipping_pre_phone_id', 'shipping_phone',
'birthday', 'website', 'facebook', 'facebook_fanpage', 'instagram', 'bank_owner', 'bank_iban', 'bank_bic', 'notice'
'm_account',
'm_salutation',
'm_first_name',
'm_last_name',
'm_notes',
'company',
'salutation',
'first_name',
'last_name',
'address',
'address_2',
'zipcode',
'city',
'country_id',
'pre_phone_id',
'phone',
'pre_mobil_id',
'mobil',
'tax_number',
'tax_identification_number',
'taxable_sales',
'same_as_billing',
'shipping_salutation',
'shipping_company',
'shipping_firstname',
'shipping_lastname',
'shipping_address',
'shipping_address_2',
'shipping_zipcode',
'shipping_city',
'shipping_country_id',
'shipping_pre_phone_id',
'shipping_phone',
'shipping_postnumber',
'birthday',
'website',
'facebook',
'facebook_fanpage',
'instagram',
'bank_owner',
'bank_iban',
'bank_bic',
'notice'
];
//'reverse_charge', 'reverse_charge_valid'
@ -191,26 +230,37 @@ class UserAccount extends Model
return Carbon::parse($value)->format(\Util::formatDateDB());
}
public function setBirthdayAttribute( $value ) {
public function setBirthdayAttribute($value)
{
$this->attributes['birthday'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getDataProtectionFormat(){
if(!$this->attributes['data_protection']){ return ""; }
public function getDataProtectionFormat()
{
if (!$this->attributes['data_protection']) {
return "";
}
return Carbon::parse($this->attributes['data_protection'])->format(\Util::formatDateTimeDB());
}
public function getAcceptContractFormat(){
if(!$this->attributes['accept_contract']){ return ""; }
public function getAcceptContractFormat()
{
if (!$this->attributes['accept_contract']) {
return "";
}
return Carbon::parse($this->attributes['accept_contract'])->format(\Util::formatDateTimeDB());
}
public function getReverseChargeValidFormat(){
if(!$this->attributes['reverse_charge_valid']){ return ""; }
public function getReverseChargeValidFormat()
{
if (!$this->attributes['reverse_charge_valid']) {
return "";
}
return Carbon::parse($this->attributes['reverse_charge_valid'])->format(\Util::formatDateTimeDB());
}
public function getCountryAttrAs($attr, $as = false){
public function getCountryAttrAs($attr, $as = false)
{
if ($this->country) {
$val = $this->country->getAttrByKey($attr);
@ -219,48 +269,52 @@ class UserAccount extends Model
return $as;
}
return true;
}
}
return "";
}
public function setNotice($key, $value){
public function setNotice($key, $value)
{
$notice = $this->notice;
$notice[$key] = $value;
$this->notice = $notice;
$this->save();
}
public function getNotice($key){
public function getNotice($key)
{
return isset($this->notice[$key]) ? $this->notice[$key] : false;
}
public function getPhoneNumber(){
public function getPhoneNumber()
{
if ($this->mobil && $this->mobil !== "") {
return ($this->pre_mobil ? $this->pre_mobil->phone : '') . " " . $this->mobil;
}
if ($this->phone && $this->phone !== "") {
return ($this->pre_phone ? $this->pre_phone->phone : '') . " " . $this->phone;
}
}
public function getPhoneFull(){
public function getPhoneFull()
{
if ($this->phone && $this->phone !== "") {
return ($this->pre_phone ? $this->pre_phone->phone : '') . " " . $this->phone;
}
return "";
}
public function getMobilFull(){
public function getMobilFull()
{
if ($this->mobil && $this->mobil !== "") {
return ($this->pre_mobil ? $this->pre_mobil->phone : '') . " " . $this->mobil;
}
return "";
}
public function getShippingPhoneFull(){
public function getShippingPhoneFull()
{
if ($this->shipping_phone && $this->shipping_phone !== "") {
return ($this->shipping_pre_phone ? $this->shipping_pre_phone->phone : '') . " " . $this->shipping_phone;
}

View file

@ -137,25 +137,29 @@ class UserBusiness extends Model
'm_level_id' => 'int',
'active_account' => 'bool',
'm_account' => 'int',
'sales_volume_KP_points' => 'int',
'sales_volume_TP_points' => 'int',
'sales_volume_points_shop' => 'int',
'sales_volume_points_KP_sum' => 'int',
'sales_volume_points_TP_sum' => 'int',
'sales_volume_KP_points' => 'float',
'sales_volume_TP_points' => 'float',
'sales_volume_points_shop' => 'float',
'sales_volume_points_KP_sum' => 'float',
'sales_volume_points_TP_sum' => 'float',
'sales_volume_total' => 'float',
'sales_volume_total_shop' => 'float',
'sales_volume_total_sum' => 'float',
'payline_points' => 'int',
'payline_points_qual_kp' => 'int',
'active_growth_bonus' => 'float',
'payline_points' => 'float',
'payline_points_qual_kp' => 'float',
'margin' => 'float',
'margin_shop' => 'float',
'qual_kp' => 'int',
'qual_pp' => 'int',
'calc_qual_kp' => 'int',
'total_pp' => 'int',
'total_qual_pp' => 'int',
'commission_pp_total' => 'float',
'commission_growth_total' => 'float',
'growth_bonus_details' => 'array',
'commission_shop_sales' => 'float',
'active_growth_bonus' => 'float',
'qual_user_level' => 'array',
'qual_user_level_next' => 'array',
'next_qual_user_level' => 'array',
@ -202,15 +206,18 @@ class UserBusiness extends Model
'margin_shop',
'qual_kp',
'qual_pp',
'calc_qual_kp',
'qual_user_level',
'qual_user_level_next',
'next_qual_user_level',
'next_can_user_level',
'total_pp',
'total_qual_pp',
'active_growth_bonus',
'commission_shop_sales',
'commission_pp_total',
'commission_growth_total',
'growth_bonus_details',
'business_lines',
'user_items',
'version',
@ -226,23 +233,60 @@ class UserBusiness extends Model
return $this->belongsTo(UserBusinessStructure::class, 'b_structure_id');
}
public function isSave(){
public function isSave()
{
return $this->id !== null ? true : false;
}
public function setPaymentAccountDateAttribute( $value ) {
public function setPaymentAccountDateAttribute($value)
{
$this->attributes['payment_account_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function setActiveDateAttribute( $value ) {
public function setActiveDateAttribute($value)
{
$this->attributes['active_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getSalesVolumeTotalMargin(){
public function getSalesVolumeTotalMargin()
{
return $this->sales_volume_total / 100 * $this->margin;
}
// Formatted Points Getter
public function getFormattedSalesVolumeKPPoints()
{
return isset($this->attributes['sales_volume_KP_points']) ? formatNumber($this->attributes['sales_volume_KP_points']) : "";
}
public function getFormattedSalesVolumeTPPoints()
{
return isset($this->attributes['sales_volume_TP_points']) ? formatNumber($this->attributes['sales_volume_TP_points']) : "";
}
public function getFormattedSalesVolumePointsShop()
{
return isset($this->attributes['sales_volume_points_shop']) ? formatNumber($this->attributes['sales_volume_points_shop']) : "";
}
public function getFormattedSalesVolumePointsKPSum()
{
return isset($this->attributes['sales_volume_points_KP_sum']) ? formatNumber($this->attributes['sales_volume_points_KP_sum']) : "";
}
public function getFormattedSalesVolumePointsTPSum()
{
return isset($this->attributes['sales_volume_points_TP_sum']) ? formatNumber($this->attributes['sales_volume_points_TP_sum']) : "";
}
public function getFormattedPaylinePoints()
{
return isset($this->attributes['payline_points']) ? formatNumber($this->attributes['payline_points']) : "";
}
public function getFormattedPaylinePointsQualKp()
{
return isset($this->attributes['payline_points_qual_kp']) ? formatNumber($this->attributes['payline_points_qual_kp']) : "";
}
}

View file

@ -69,7 +69,25 @@ class UserLevel extends Model
protected $table = 'user_levels';
protected $fillable = [
'next_id', 'name', 'margin', 'margin_shop', 'qual_kp', 'qual_pp', 'growth_bonus', 'pr_line_1', 'pr_line_2', 'pr_line_3', 'pr_line_4', 'pr_line_5', 'pr_line_6', 'pr_line_7', 'pr_line_8', 'paylines', 'pos', 'active', 'default',
'next_id',
'name',
'margin',
'margin_shop',
'qual_kp',
'qual_pp',
'growth_bonus',
'pr_line_1',
'pr_line_2',
'pr_line_3',
'pr_line_4',
'pr_line_5',
'pr_line_6',
'pr_line_7',
'pr_line_8',
'paylines',
'pos',
'active',
'default',
];
@ -83,15 +101,16 @@ class UserLevel extends Model
return $this->hasMany(TransUserLevel::class, 'user_level_id');
}
public function getNextUserLevels(){
public function getNextUserLevels()
{
//$ret = [0=>'Keinen'];
$ret = UserLevel::where('active', true)->where('id', '!=', $this->id)->orderBy('pos', 'asc')->get()->pluck('name', 'id')->toArray();
return [0 => '-> Keinen Karriere Level'] + $ret;
}
public function setPosAttribute($value){
public function setPosAttribute($value)
{
$this->attributes['pos'] = is_numeric($value) ? $value : null;
}
public function setGrowthBonusAttribute($value)
@ -100,7 +119,7 @@ class UserLevel extends Model
}
public function getFormattedGrowthBonus()
{
return isset($this->attributes['growth_bonus']) ? Util::formatNumber($this->attributes['growth_bonus'],1) : "";
return isset($this->attributes['growth_bonus']) ? Util::formatNumber($this->attributes['growth_bonus'], 2) : "";
}
public function getFormattedMargin()
@ -128,6 +147,4 @@ class UserLevel extends Model
$trans = $this->translations->where('language', '=', $lang)->where('key', $key)->first();
return $trans ? $trans->value : '';
}
}

View file

@ -79,10 +79,10 @@ class UserSalesVolume extends Model
'user_invoice_id' => 'int',
'month' => 'int',
'year' => 'int',
'points' => 'int',
'month_KP_points' => 'int',
'month_TP_points' => 'int',
'month_shop_points' => 'int',
'points' => 'float',
'month_KP_points' => 'float',
'month_TP_points' => 'float',
'month_shop_points' => 'float',
'status_points' => 'int',
'status_turnover' => 'int',
'total_net' => 'float',
@ -164,32 +164,81 @@ class UserSalesVolume extends Model
return $this->belongsTo(UserInvoice::class);
}
public function getDateAttribute(){
public function getDateAttribute()
{
return $this->attributes['date'] ? Carbon::parse($this->attributes['date'])->format(\Util::formatDateDB()) : '';
}
public function setDateAttribute( $value ) {
public function setDateAttribute($value)
{
$this->attributes['date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : NULL;
}
public function getDateRaw(){
public function getDateRaw()
{
return isset($this->attributes['date']) ? $this->attributes['date'] : NULL;
}
public function getPointsKPSum(){
// Points Setter/Getter für deutsches Zahlenformat
public function setPointsAttribute($value)
{
$this->attributes['points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function setMonthKPPointsAttribute($value)
{
$this->attributes['month_KP_points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function setMonthTPPointsAttribute($value)
{
$this->attributes['month_TP_points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function setMonthShopPointsAttribute($value)
{
$this->attributes['month_shop_points'] = $value !== null ? \Util::reFormatNumber($value) : null;
}
public function getFormattedPoints()
{
return isset($this->attributes['points']) ? \Util::formatNumber($this->attributes['points']) : "";
}
public function getFormattedMonthKPPoints()
{
return isset($this->attributes['month_KP_points']) ? \Util::formatNumber($this->attributes['month_KP_points']) : 0;
}
public function getFormattedMonthTPPoints()
{
return isset($this->attributes['month_TP_points']) ? \Util::formatNumber($this->attributes['month_TP_points']) : 0;
}
public function getFormattedMonthShopPoints()
{
return isset($this->attributes['month_shop_points']) ? \Util::formatNumber($this->attributes['month_shop_points']) : 0;
}
public function getPointsKPSum()
{
return $this->month_KP_points + $this->month_shop_points; //only KP für SUM - KP is for User
}
public function getPointsTPSum(){
public function getPointsTPSum()
{
return $this->month_TP_points + $this->month_shop_points; //only TP für SUM - TP is only for Payline
}
public function getTotalNetSum(){
public function getTotalNetSum()
{
return $this->month_total_net + $this->month_shop_total_net;
}
public function getStatusType(){
public function getStatusType()
{
return isset(self::$statusTypes[$this->status]) ? __('payment.' . self::$statusTypes[$this->status]) : "";
}
public static function getTransStatusType(){
public static function getTransStatusType()
{
$ret = [];
foreach (self::$statusTypes as $key => $val) {
$ret[$key] = trans('payment.' . $val);
@ -197,7 +246,8 @@ class UserSalesVolume extends Model
return $ret;
}
public static function getTransTurnoverTypes(){
public static function getTransTurnoverTypes()
{
$ret = [];
foreach (self::$statusTurnoverTypes as $key => $val) {
$ret[$key] = trans('payment.' . $val);
@ -205,18 +255,22 @@ class UserSalesVolume extends Model
return $ret;
}
public function getStatusColor(){
public function getStatusColor()
{
return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : "default";
}
public function getStatusPointsType(){
public function getStatusPointsType()
{
return isset(self::$statusPointsTypes[$this->status_points]) ? self::$statusPointsTypes[$this->status_points] : "";
}
public function getStatusPointsColor(){
public function getStatusPointsColor()
{
return isset(self::$statusColors[$this->status_points]) ? self::$statusColors[$this->status_points] : "default";
}
public function getStatusTurnoverType(){
public function getStatusTurnoverType()
{
switch ($this->status) {
case 1: //Bestellung Berater
return 'E';
@ -233,7 +287,8 @@ class UserSalesVolume extends Model
}
return "";
}
public function getStatusTurnoverColor(){
public function getStatusTurnoverColor()
{
switch ($this->status) {
@ -253,18 +308,21 @@ class UserSalesVolume extends Model
return "default";
}
public function getFormatedMonthYear(){
public function getFormatedMonthYear()
{
return str_pad($this->month, 2, "0", STR_PAD_LEFT) . "/" . $this->year;
}
public function isCurrentMonthYear(){
public function isCurrentMonthYear()
{
if ($this->month === intval(date('m')) && $this->year === intval(date('Y'))) {
return true;
}
return false;
}
public function caluCommissonTotalNet($margin){
public function caluCommissonTotalNet($margin)
{
if ($this->total_net > 0 && $margin > 0) {
return $this->total_net / 100 * $margin;
}

View file

@ -6,7 +6,8 @@ use Carbon;
use App\Models\UserAbo;
use App\Services\AboHelper;
class AboRepository extends BaseRepository {
class AboRepository extends BaseRepository
{
public function __construct()
@ -15,7 +16,8 @@ class AboRepository extends BaseRepository {
}
public function setModel(UserAbo $model){
public function setModel(UserAbo $model)
{
$this->model = $model;
}
@ -37,29 +39,42 @@ class AboRepository extends BaseRepository {
return false;
}
public function create($data){
public function create($data) {}
private function updateStatus($data)
{
// Handle cancellation
if (isset($data['abo_cancel']) && $data['abo_cancel'] == 'true') {
// Status 4 = abo_cancel (storniert/gekündigt)
$this->model->status = 4;
$this->model->active = false;
$this->model->cancel_date = now();
$this->model->save();
return;
}
private function updateStatus($data){
$active = (isset($data['abo_is_active']) && $data['abo_is_active']) ? true : false;
//if status is active and active is false, set status to inactive
if ($this->model->active && !$active) {
if($this->model->status = 2){ //okay
$this->model->status = 6; //
if ($this->model->status == 2) { //okay
$this->model->status = 6; //inactive
$this->model->active = false;
$this->model->save();
}
}
if (!$this->model->active && $active) {
if ($this->model->status = 6) { //inactive
$this->model->status = 2; //okay
$this->model->active = true;
$this->model->save();
}
}
$this->model->active = $active;
return;
}
private function validate($data){
private function validate($data)
{
if ($data['view'] !== 'admin') {
if ($this->model->is_for === 'me' && $this->model->user_id !== \Auth::user()->id) {
\Session()->flash('alert-error', 'Unauthorized action. User ID does not match.');
@ -83,7 +98,17 @@ class AboRepository extends BaseRepository {
\Session()->flash('alert-error', __('abo.error_abo_interval'));
return false;
}
return true;
// Prüfung: Wenn das Abo diesen Monat noch nicht ausgeführt wurde (oder noch nie),
// darf das Abo-Intervall nicht auf einen Tag gesetzt werden, der bereits vergangen ist (oder heute ist),
// da setNextDate das nächste Ausführungsdatum sonst auf den nächsten Monat setzt und dieser Monat übersprungen wird.
$executedThisMonth = $this->model->last_date && \Carbon\Carbon::parse($this->model->last_date)->isCurrentMonth();
if (!$executedThisMonth && $data['abo_interval'] <= now()->day) {
\Session()->flash('alert-error', __('abo.error_abo_interval_in_the_past'));
return false;
}
return true;
}
}

View file

@ -14,7 +14,8 @@ use App\Models\ShoppingOrderItem;
use Illuminate\Support\Collection;
class CheckoutRepository extends BaseRepository {
class CheckoutRepository extends BaseRepository
{
private $session;
private $instance;
@ -26,7 +27,8 @@ class CheckoutRepository extends BaseRepository {
$this->instance = 'checkout';
}
public function makeShoppingOrder($shopping_user, $data){
public function makeShoppingOrder($shopping_user, $data)
{
$user_shop = Util::getUserShop();
@ -50,7 +52,7 @@ class CheckoutRepository extends BaseRepository {
'subtotal_ws' => 0,
'tax' => $total - $homeparty->order['ek_price_net'],
'total_shipping' => Yard::instance($this->instance)->totalWithShipping(2, '.', ''),
'points' => $homeparty->order['points'] - $homeparty->order['bonus_points_diff'],
'points' => round($homeparty->order['points'] - $homeparty->order['bonus_points_diff'], 2),
'weight' => 0,
'txaction' => 'prev',
'mode' => Util::getUserShoppingMode(),
@ -75,7 +77,7 @@ class CheckoutRepository extends BaseRepository {
'tax' => $ShoppingCollectOrder->tax_total,
'tax_split' => $ShoppingCollectOrder->tax_split,
'total_shipping' => Yard::instance($this->instance)->totalWithShipping(2, '.', ''),
'points' => $ShoppingCollectOrder->points,
'points' => round($ShoppingCollectOrder->points, 2),
'weight' => 0,
'txaction' => 'prev',
'mode' => Util::getUserShoppingMode(),
@ -95,7 +97,7 @@ class CheckoutRepository extends BaseRepository {
'subtotal_ws' => Yard::instance($this->instance)->subtotalWithShipping(2, '.', ''),
'tax' => Yard::instance($this->instance)->taxWithShipping(2, '.', ''),
'total_shipping' => Yard::instance($this->instance)->totalWithShipping(2, '.', ''),
'points' => Yard::instance($this->instance)->points(),
'points' => round(Yard::instance($this->instance)->points(), 2),
'weight' => Yard::instance($this->instance)->weight(),
'is_abo' => isset($data['is_abo']) ? $data['is_abo'] : false,
'abo_interval' => isset($data['abo_interval']) ? $data['abo_interval'] : null,
@ -200,7 +202,8 @@ class CheckoutRepository extends BaseRepository {
}
return $shopping_order;
}
public function makeShoppingUser($data){
public function makeShoppingUser($data)
{
$data['same_as_billing'] = isset($data['same_as_billing']) ? false : true; //reinvert
$data['accepted_data_checkbox'] = isset($data['accepted_data_checkbox']) ? true : false;
@ -221,7 +224,8 @@ class CheckoutRepository extends BaseRepository {
return $shopping_user;
}
public function getPaymentsMethods($is_from, $is_abo = false){
public function getPaymentsMethods($is_from, $is_abo = false)
{
$payment_methods = [];
if ($is_from !== 'shopping' && Util::getAuthUser()) {
$user = Util::getAuthUser();
@ -239,7 +243,8 @@ class CheckoutRepository extends BaseRepository {
return $payment_methods;
}
public function isPaymentsMethodsActive($payment_method, $is_from, $is_abo = false){
public function isPaymentsMethodsActive($payment_method, $is_from, $is_abo = false)
{
$payment_names = ['wlt#PPE' => 'PP', 'cc' => 'CC', 'sb#PNT' => 'SB', 'elv' => 'SEPA', 'vor' => 'VOR', 'fnc#MIV' => 'FNC'];
$payment_methods = $this->getPaymentsMethods($is_from, $is_abo);
if (isset($payment_names[$payment_method])) {
@ -251,7 +256,8 @@ class CheckoutRepository extends BaseRepository {
abort(404);
}
public function makeCustomerShoppingUser($shopping_data, $is_for, $is_from){
public function makeCustomerShoppingUser($shopping_data, $is_for, $is_from)
{
// $shopping_user = ShoppingUser::findOrFail($shopping_data['shopping_user_id']);
$shopping_user = new ShoppingUser();
$shopping_user->fill($shopping_data);
@ -266,7 +272,6 @@ class CheckoutRepository extends BaseRepository {
$shopping_user->mode = 'prev';
$shopping_user->language = \App::getLocale();
return $shopping_user;
}
public function initShoppingUser($is_for, $is_from, $homeparty_id = null)
@ -288,7 +293,8 @@ class CheckoutRepository extends BaseRepository {
return $shopping_user;
}
public function shoppingUserByAuthCustomer(\App\Models\Customer $user){
public function shoppingUserByAuthCustomer(\App\Models\Customer $user)
{
//clone shopping user!
if ($user->shopping_user_id) {
@ -306,7 +312,8 @@ class CheckoutRepository extends BaseRepository {
return $shopping_user;
}
public function shoppingUserByAuthUser(\App\User $user, $is_from, $is_for){
public function shoppingUserByAuthUser(\App\User $user, $is_from, $is_for)
{
$shopping_user = new ShoppingUser();
$shopping_user->language = \App::getLocale();
@ -340,12 +347,14 @@ class CheckoutRepository extends BaseRepository {
$shopping_user->shipping_city = $user->account->shipping_city;
//$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
return $shopping_user;
}
public function shoppingUserAuthData($is_from, $is_for, $data = []){
public function shoppingUserAuthData($is_from, $is_for, $data = [])
{
$user = Util::getAuthUser();
$shopping_user = new ShoppingUser();
@ -407,7 +416,7 @@ class CheckoutRepository extends BaseRepository {
$shopping_user->shipping_city = isset($data['shipping_city']) ? $data['shipping_city'] : '';
$shopping_user->shipping_country_id = Yard::instance($this->instance)->getShippingCountryCountryId();
$shopping_user->shipping_phone = isset($data['shipping_phone']) ? $data['shipping_phone'] : '';
$shopping_user->shipping_postnumber = isset($data['shipping_postnumber']) ? $data['shipping_postnumber'] : '';
} else {
$shopping_user->same_as_billing = $user->account->same_as_billing ? false : true;
$shopping_user->shipping_salutation = $user->account->shipping_salutation;
@ -420,19 +429,21 @@ class CheckoutRepository extends BaseRepository {
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
}
return $shopping_user;
}
public function putSessionPayments($key, $value){
public function putSessionPayments($key, $value)
{
$content = $this->getContent();
$content->put($key, $value);
$this->session->put($this->instance, $content);
}
public function getSessionPayments($key){
public function getSessionPayments($key)
{
$content = $this->getContent();
if ($content->has($key)) {

View file

@ -2,7 +2,10 @@
namespace App\Repositories;
class CustomerRepository extends BaseRepository {
use App\Models\ShoppingUser;
class CustomerRepository extends BaseRepository
{
public function __construct()
@ -10,7 +13,42 @@ class CustomerRepository extends BaseRepository {
//$this->model = $model;
}
public function deleteCustomer(ShoppingUser $shopping_user)
{
if ($shopping_user->shopping_orders->count() > 0) {
return false;
}
$shopping_user->auth_user_id = null;
$shopping_user->member_id = null;
$shopping_user->billing_salutation = null;
$shopping_user->billing_company = null;
$shopping_user->billing_firstname = 'Deleted';
$shopping_user->billing_lastname = 'User';
$shopping_user->billing_address = 'Deleted';
$shopping_user->billing_address_2 = null;
$shopping_user->billing_zipcode = '00000';
$shopping_user->billing_city = 'Deleted';
$shopping_user->billing_phone = null;
$shopping_user->billing_email = 'deleted_' . $shopping_user->id . '@deleted.com';
$shopping_user->shipping_salutation = null;
$shopping_user->shipping_company = null;
$shopping_user->shipping_firstname = 'Deleted';
$shopping_user->shipping_lastname = 'User';
$shopping_user->shipping_address = 'Deleted';
$shopping_user->shipping_address_2 = null;
$shopping_user->shipping_zipcode = '00000';
$shopping_user->shipping_city = 'Deleted';
$shopping_user->shipping_phone = null;
$shopping_user->shipping_email = 'deleted_' . $shopping_user->id . '@deleted.com';
$shopping_user->remarks = null;
$shopping_user->notice = null;
$shopping_user->faker_mail = true;
$shopping_user->save();
return true;
}
public function update($data)
{
@ -44,7 +82,8 @@ class CustomerRepository extends BaseRepository {
return true;
}
public function create($data){
public function create($data)
{
/* $this->model = User::create([
'email' => $data['email'],
@ -66,6 +105,4 @@ class CustomerRepository extends BaseRepository {
return $this->model;*/
}
}

View file

@ -2,24 +2,21 @@
namespace App\Repositories;
use App\Models\CountryPrice;
use App\Models\Product;
use App\Models\ProductAttribute;
use App\Models\ProductBundle;
use App\Models\ProductCategory;
use App\Models\ProductImage;
use App\Models\ProductIngredient;
class ProductRepository extends BaseRepository {
class ProductRepository extends BaseRepository
{
public function __construct(Product $model)
{
$this->model = $model;
}
/**
* refresh.
*/
@ -34,27 +31,26 @@ class ProductRepository extends BaseRepository {
$data['sponsor_buying_points'] = isset($data['sponsor_buying_points']) ? 1 : 0;
$data['show_on'] = isset($data['show_on']) ? $data['show_on'] : null;
if($data['id'] === "new"){
if ($data['id'] === 'new') {
$this->model = Product::create($data);
}
else{
} else {
$this->model = $this->getById($data['id']);
$this->model->slug = null;
$this->model->fill($data);
$this->model->save();
}
$this->updateCategories(isset($data['categories']) ? $data['categories'] : array());
$this->updateAttributes(isset($data['attributes']) ? $data['attributes'] : array());
$this->updateIngredients(isset($data['product_ingredients']) ? $data['product_ingredients'] : array());
$this->updateCategories(isset($data['categories']) ? $data['categories'] : []);
$this->updateAttributes(isset($data['attributes']) ? $data['attributes'] : []);
$this->updateIngredients(isset($data['product_ingredients']) ? $data['product_ingredients'] : []);
$this->updateBundles(isset($data['product_bundles']) ? $data['product_bundles'] : []);
$this->updateBundleQuantities(isset($data['bundle_quantities']) ? $data['bundle_quantities'] : []);
$this->updateCountryPrices($data);
return $this->model;
}
public function updateIngredients($data = array())
public function updateIngredients($data = [])
{
$ProductIngredient = $this->model->p_ingredients()->pluck('ingredient_id')->toArray();
// set attr
@ -69,13 +65,92 @@ class ProductRepository extends BaseRepository {
}
}
}
return true;
}
public function updateCategories($data = array())
/**
* Aktualisiert Bundle-Produkte (Set/Kit-Inhalte)
* Fügt nur neue Bundle-Items hinzu, löscht keine bestehenden
*
* @param array $data Array von Produkt-IDs die zum Bundle hinzugefügt werden sollen
* @return bool
*/
public function updateBundles($data = [])
{
$existingBundleIds = $this->model->bundleItems()->pluck('bundle_product_id')->toArray();
if (is_array($data)) {
$pos = count($existingBundleIds);
foreach ($data as $bundleProductId) {
// Nur hinzufügen wenn noch nicht vorhanden und nicht das Produkt selbst
if (! in_array($bundleProductId, $existingBundleIds) && $bundleProductId != $this->model->id) {
ProductBundle::create([
'product_id' => $this->model->id,
'bundle_product_id' => $bundleProductId,
'quantity' => 1,
'pos' => $pos++,
]);
}
}
}
return true;
}
/**
* Aktualisiert die Menge eines Bundle-Items
*
* @param int $bundleProductId ID des enthaltenen Produkts
* @param int $quantity Neue Menge
* @return bool
*/
public function updateBundleQuantity($bundleProductId, $quantity)
{
$bundle = ProductBundle::where('product_id', $this->model->id)
->where('bundle_product_id', $bundleProductId)
->first();
if ($bundle) {
$bundle->quantity = max(1, intval($quantity));
$bundle->save();
return true;
}
return false;
}
/**
* Aktualisiert die Mengen aller Bundle-Items
*
* @param array $data Array mit bundle_product_id => quantity Zuordnungen
* @return bool
*/
public function updateBundleQuantities($data = [])
{
if (is_array($data)) {
foreach ($data as $bundleProductId => $item) {
if (isset($item['qty']) && isset($item['id'])) {
$bundle = ProductBundle::where('product_id', $this->model->id)
->where('bundle_product_id', $item['id'])
->first();
if ($bundle) {
$bundle->quantity = max(1, intval($item['qty']));
$bundle->save();
}
}
}
}
return true;
}
public function updateCategories($data = [])
{
foreach ($this->model->categories as $category) {
if(($pos = array_search($category->category_id, $data)) !== FALSE){
if (($pos = array_search($category->category_id, $data)) !== false) {
unset($data[$pos]);
} else {
$category->delete();
@ -90,13 +165,14 @@ class ProductRepository extends BaseRepository {
]);
}
}
return true;
}
public function updateAttributes($data = array())
public function updateAttributes($data = [])
{
foreach ($this->model->attributes as $attribute) {
if(($pos = array_search($attribute->attribute_id, $data)) !== FALSE){
if (($pos = array_search($attribute->attribute_id, $data)) !== false) {
unset($data[$pos]);
} else {
$attribute->delete();
@ -111,6 +187,7 @@ class ProductRepository extends BaseRepository {
]);
}
}
return true;
}
@ -120,7 +197,8 @@ class ProductRepository extends BaseRepository {
return false;
}
foreach ($data['country_prices'] as $k => $country_id) {
$cp = CountryPrice::updateOrCreate([
$cp = CountryPrice::updateOrCreate(
[
'country_id' => $country_id,
'product_id' => $this->model->id,
],
@ -129,20 +207,17 @@ class ProductRepository extends BaseRepository {
'c_tax' => isset($data['c_tax'][$country_id]) ? reFormatNumber($data['c_tax'][$country_id]) : null,
'c_price_old' => isset($data['c_price_old'][$country_id]) ? reFormatNumber($data['c_price_old'][$country_id]) : null,
'c_currency' => isset($data['c_currency'][$country_id]) ? reFormatNumber($data['c_currency'][$country_id]) : null,
]);
]
);
}
return true;
}
public function copy($model)
{
$this->model = $model->replicate();
$this->model->name = "Kopie: ".$this->model->name;
$this->model->name = 'Kopie: ' . $this->model->name;
$this->model->wp_number = null;
$this->model->save();
@ -173,30 +248,18 @@ class ProductRepository extends BaseRepository {
'images/product/' . $this->model->id . '/' . $name
);
ProductImage::create([
'product_id' => $this->model->id,
'filename' => $name,
'original_name' => $image->original_name,
'ext' => $image->ext,
'mine' => $image->mine,
'size' => $image->size
'size' => $image->size,
]);
}
return $this->model;
}
public function delete()
{
}
public function delete() {}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\Product;
@ -29,41 +30,69 @@ class AboHelper
];
public static function userHasAbo(User $user){
public static function userHasAbo(User $user)
{
$user = $user ? $user : \Auth::user();
return UserAbo::where('user_id', $user->id)->where('is_for', 'me')->where('status', '>', 1)->first() === null ? false : true;
}
public static function memberHasAbo(ShoppingUser $shopping_user){
public static function memberHasAbo(ShoppingUser $shopping_user)
{
if (!$shopping_user) {
return false;
}
return UserAbo::where('email', $shopping_user->billing_email)->where('is_for', 'ot')->where('status', '>', 1)->first() === null ? false : true;
}
public static function hasAboByEmail($email){
public static function hasAboByEmail($email)
{
return UserAbo::where('email', $email)->where('status', '>', 1)->first() === null ? false : true;
}
public static function setAboStatus(ShoppingOrder $shopping_order, $status){
public static function setAboStatus(ShoppingOrder $shopping_order, $status, $paid = false)
{
$user_abo = $shopping_order->getUserAbo();
if ($user_abo && $user_abo->status < 2) { //status < 2 is not active
$user_abo->update(['status' => $status]);
}
UserAboOrder::where('user_abo_id', $user_abo->id)->where('shopping_order_id', $shopping_order->id)->update(['status' => $status]);
UserAboOrder::where('user_abo_id', $user_abo->id)->where('shopping_order_id', $shopping_order->id)->update(['status' => $status, 'paid' => $paid]);
}
public static function setAboActive(ShoppingOrder $shopping_order, $status){
self::setAboStatus($shopping_order, $status);
public static function setAboActive(ShoppingOrder $shopping_order, $status, $paid = false)
{
self::setAboStatus($shopping_order, $status, $paid);
//delete UserAbo is not active status = 1
//is_for = me
UserAbo::where('user_id', $shopping_order->auth_user_id)->where('is_for', 'me')->where('status', 1)->delete();
//is_for = ot
UserAbo::where('member_id', $shopping_order->member_id)->where('email', $shopping_order->shopping_user->billing_email)->where('is_for', 'ot')->where('status', 1)->delete();
}
public static function aboHasBaseProduct($yard_products){
public static function getAboMinDuration()
{
return \App\Models\Setting::getContentBySlug('abo-min-duration');
}
public static function canCancelAbo(UserAbo $user_abo, $view = 'user')
{
$minDuration = self::getAboMinDuration();
if ($view === 'admin') {
return true;
}
$paidOrdersCount = $user_abo->getCountPaidOrders();
return $paidOrdersCount >= (int) $minDuration;
}
public static function canEditAbo($user_abo, $view = 'user')
{
if ($view !== 'admin' && ($user_abo->user_id != \Auth::user()->id && $user_abo->member_id != \Auth::user()->id)) {
return false;
}
return true;
}
public static function aboHasBaseProduct($yard_products)
{
foreach ($yard_products as $product) {
if (is_array($product->options->show_on)) {
if (in_array('12', $product->options->show_on)) {
@ -73,7 +102,8 @@ class AboHelper
}
return false;
}
public static function getAboShowOn(Product $product){
public static function getAboShowOn(Product $product)
{
$show_on = $product->show_on;
if (in_array('12', $show_on)) {
return 'base';
@ -83,7 +113,8 @@ class AboHelper
}
return false;
}
public static function getAboTypeBadge($abo_type){
public static function getAboTypeBadge($abo_type)
{
if ($abo_type === 'base') {
return '<span class="badge badge-pill badge-warning"><i class="fas fa-star"></i> ' . __('abo.' . $abo_type) . '</span></a>';
}
@ -93,17 +124,30 @@ class AboHelper
return '';
}
public static function setNextDate($date, $abo_interval){
public static function setNextDate($date, $abo_interval)
{
$nextDate = Carbon::parse($date)->firstOfMonth();
$nextDate->addDays($abo_interval - 1);
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
}
public static function createNewAbo(ShoppingPayment $shopping_payment){
public static function getFirstAboDate($date, $abo_interval)
{
$nextDate = Carbon::parse($date)->firstOfMonth()->addMonth(1);
$nextDate->addDays($abo_interval - 1);
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
}
public static function createNewAbo(ShoppingPayment $shopping_payment)
{
//is Abo - create init Abo from PP or else
if ($shopping_payment->shopping_order->is_abo && $shopping_payment->shopping_order->abo_interval > 0) {
$payment_transaction = $shopping_payment->payment_transactions->last();
//next_date immer im nächsten Monat starten
//is auth_user_id = Berater bestellung
//is member_id = Kunden bestellung
//is for = me = mich oder ot = kunde
$user_abo = UserAbo::create([
'user_id' => $shopping_payment->shopping_order->auth_user_id,
'member_id' => $shopping_payment->shopping_order->member_id,
@ -119,7 +163,7 @@ class AboHelper
'abo_interval' => $shopping_payment->abo_interval,
'start_date' => now(),
'last_date' => now(),
'next_date' => self::setNextDate(now(), $shopping_payment->abo_interval),
'next_date' => self::getFirstAboDate(now(), $shopping_payment->abo_interval),
]);
if ($user_abo) {
@ -131,10 +175,10 @@ class AboHelper
]);
}
}
}
public static function createAboItems($user_abo, ShoppingPayment $shopping_payment){
public static function createAboItems($user_abo, ShoppingPayment $shopping_payment)
{
foreach ($shopping_payment->shopping_order->shopping_order_items as $item) {
UserAboItem::create([
'user_abo_id' => $user_abo->id,
@ -147,12 +191,12 @@ class AboHelper
}
public static function getTransStatusFilterText(){
public static function getTransStatusFilterText()
{
$ret = [];
foreach (self::$txaction_filter_text as $key => $val) {
$ret[$key] = trans('payment.' . $val);
}
return $ret;
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use Yard;
@ -20,10 +21,30 @@ class AboOrderCart
private static $customer_detail;
public static function initYard($user_abo){
public static function initYard($user_abo)
{
// WICHTIG: Statische Variablen zurücksetzen, um sicherzustellen, dass keine Daten
// aus vorherigen Abos verwendet werden
self::$user_abo = $user_abo;
Yard::instance('shopping')->destroy();
self::$is_for = null;
self::$customer_detail = null;
// Yard komplett leeren - wichtig für Batch-Verarbeitung mehrerer Abos
$yard = Yard::instance('shopping');
$itemsBeforeDestroy = $yard->content()->count();
$yard->destroy();
\Log::info('AboOrderCart::initYard: Yard geleert', [
'abo_id' => $user_abo->id,
'items_vor_destroy' => $itemsBeforeDestroy
]);
$itemsAfterDestroy = $yard->content()->count();
\Log::info('AboOrderCart::initYard: Yard geleert', [
'abo_id' => $user_abo->id,
'items_after_destroy' => $itemsAfterDestroy
]);
self::$customer_detail = self::makeCustomerDetail($user_abo);
if ($user_abo->is_for === 'me') {
self::$is_for = 'abo-me';
@ -44,15 +65,14 @@ class AboOrderCart
self::$is_for = 'abo-ot-customer';
UserService::initCustomerYard(self::$customer_detail, 'abo-ot-customer');
return true;
}
return false;
}
public static function makeOrderYard($user_abo)
{
// WICHTIG: Statische Variablen explizit setzen für dieses Abo
self::$user_abo = $user_abo;
if ($user_abo->is_for === 'ot') {
self::$is_for = 'abo-ot-customer';
@ -60,7 +80,37 @@ class AboOrderCart
if ($user_abo->is_for === 'me') {
self::$is_for = 'abo-me';
}
foreach($user_abo->user_abo_items as $abo_item){
// WICHTIG: Yard IMMER leeren, um sicherzustellen, dass keine Produkte aus vorherigen Aufrufen vorhanden sind
// Dies ist besonders wichtig bei wiederholten Aufrufen der detail-Funktion (z.B. durch AJAX-Requests)
$yard = Yard::instance('shopping');
$itemsBefore = $yard->content()->count();
$yard->destroy();
if ($itemsBefore > 0) {
\Log::warning('AboOrderCart::makeOrderYard: Yard war nicht leer vor makeOrderYard und wurde geleert', [
'abo_id' => $user_abo->id,
'items_before' => $itemsBefore
]);
}
// Sicherstellen, dass die Items für dieses spezifische Abo geladen werden
// Verwende fresh() um sicherzustellen, dass wir die aktuellen Daten haben
$abo_items = $user_abo->user_abo_items()->get();
\Log::info('AboOrderCart::makeOrderYard: Füge Produkte zum Cart hinzu', [
'abo_id' => $user_abo->id,
'item_count' => $abo_items->count(),
'items' => $abo_items->map(function ($item) {
return [
'id' => $item->id,
'product_id' => $item->product_id,
'qty' => $item->qty,
'comp' => $item->comp
];
})->toArray()
]);
foreach ($abo_items as $abo_item) {
self::addProductToCart($abo_item);
}
Yard::instance('shopping')->reCalculateShippingPrice();
@ -69,7 +119,8 @@ class AboOrderCart
$user_abo->save();
}
private static function addProductToCart($item){
private static function addProductToCart($item)
{
$product = Product::find($item->product_id);
$tax_free = Yard::instance('shopping')->getUserTaxFree();
@ -77,22 +128,47 @@ class AboOrderCart
if ($product) {
if ($item->comp) {
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, 0, false, false,
['image' => '', 'slug' => $product->slug, 'weight' => 0, 'points' => 0,
'comp' => $item->comp, 'product_id' => $product->id]);
$cartItem = Yard::instance('shopping')->add(
$product->id,
$product->getLang('name'),
1,
0,
false,
false,
[
'image' => '',
'slug' => $product->slug,
'weight' => 0,
'points' => 0,
'comp' => $item->comp,
'product_id' => $product->id
]
);
Yard::setTax($cartItem->rowId, 0);
return true;
}
if (self::$is_for === 'ot-customer' || self::$is_for === 'abo-ot-customer') {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), $item->qty,
round($product->getPriceWith($tax_free, false, $user_country, false, self::$user_abo->user), 1), false, false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
->add(
$product->id,
$product->getLang('name'),
$item->qty,
round($product->getPriceWith($tax_free, false, $user_country, false, self::$user_abo->user), 1),
false,
false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
} else {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), $item->qty,
$product->getPriceWith($tax_free, true, $user_country, false, self::$user_abo->user), false, false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
->add(
$product->id,
$product->getLang('name'),
$item->qty,
$product->getPriceWith($tax_free, true, $user_country, false, self::$user_abo->user),
false,
false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
}
if ($tax_free) {
Yard::setTax($cartItem->rowId, 0);
@ -102,7 +178,8 @@ class AboOrderCart
}
}
public static function checkNumOfCompProducts($user_abo){
public static function checkNumOfCompProducts($user_abo)
{
if ($user_abo->is_for === 'me') {
$needNumComp = Yard::instance('shopping')->getNumComp();
@ -143,17 +220,33 @@ class AboOrderCart
}
}
public static function getCustomerDetail(){
public static function getCustomerDetail()
{
return self::$customer_detail;
}
/* Need this, can change the address */
public static function makeCustomerDetail($user_abo){
public static function makeCustomerDetail($user_abo)
{
if ($user_abo->is_for === 'me') {
//only on Abo!
$user = $user_abo->user;
// WICHTIG: Wenn bereits ein shopping_user existiert, diesen replizieren um alle Felder zu behalten
// Ansonsten neues Objekt erstellen
if ($user_abo->shopping_user) {
$shopping_user = $user_abo->shopping_user->replicate();
\Log::info('AboOrderCart::makeCustomerDetail: ShoppingUser repliziert für Abo ID: ' . $user_abo->id, [
'abo_id' => $user_abo->id,
'original_shopping_user_id' => $user_abo->shopping_user->id
]);
} else {
$shopping_user = new ShoppingUser();
\Log::info('AboOrderCart::makeCustomerDetail: Neuer ShoppingUser erstellt für Abo ID: ' . $user_abo->id);
}
// Account-Daten überschreiben/aktualisieren
$shopping_user->billing_salutation = $user->account->salutation;
$shopping_user->billing_company = $user->account->company;
$shopping_user->billing_firstname = $user->account->first_name;
@ -164,7 +257,12 @@ class AboOrderCart
$shopping_user->billing_city = $user->account->city;
$shopping_user->billing_country_id = $user->account->country_id;
$shopping_user->billing_phone = $user->account->phone;
$shopping_user->billing_email = $user->email ?? null;
// Auth User ID setzen falls noch nicht gesetzt
if (!$shopping_user->auth_user_id) {
$shopping_user->auth_user_id = $user->id;
}
if ($user->account->same_as_billing) {
$shopping_user->shipping_salutation = $user->account->salutation;
$shopping_user->shipping_company = $user->account->company;
@ -176,6 +274,8 @@ class AboOrderCart
$shopping_user->shipping_city = $user->account->city;
$shopping_user->shipping_country_id = $user->account->country_id;
$shopping_user->shipping_phone = $user->account->phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->same_as_billing = 1;
} else {
$shopping_user->shipping_salutation = $user->account->shipping_salutation;
$shopping_user->shipping_company = $user->account->shipping_company;
@ -187,8 +287,9 @@ class AboOrderCart
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->same_as_billing = 0;
}
}
if ($user_abo->is_for === 'ot') {
@ -197,5 +298,4 @@ class AboOrderCart
}
return $shopping_user;
}
}

View file

@ -8,9 +8,7 @@ use Carbon\Carbon;
use App\Models\UserLevel;
use App\Models\UserBusiness;
use App\Models\UserAccount;
use App\Models\UserSalesVolume;
use App\Services\TranslationHelper;
use App\Models\UserBusinessStructure;
use Illuminate\Support\Facades\Log;
/**
@ -31,6 +29,7 @@ class BusinessUserItemOptimized
private ?TreeCalcBotOptimized $treeCalcBot = null;
private $user_level_active_pos;
private $needsQualificationRecalculation = false;
private $qualificationCalculated = false;
public function __construct($date, ?TreeCalcBotOptimized $treeCalcBot = null)
{
@ -40,6 +39,12 @@ class BusinessUserItemOptimized
return $this;
}
public function isQualificationCalculated(): bool
{
return $this->qualificationCalculated;
}
/**
* Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität)
*
@ -107,6 +112,7 @@ class BusinessUserItemOptimized
*/
public function makeUserFromModel(User $user, bool $forceLiveCalculation = false): void
{
\Log::debug("BusinessUserItemOptimized: makeUserFromModel for user {$user->id} ({$this->date->month}/{$this->date->year})");
try {
if (!$user || !$user->id) {
throw new \InvalidArgumentException('Invalid user model provided');
@ -121,7 +127,6 @@ class BusinessUserItemOptimized
if ($this->b_user !== null) {
\Log::debug("BusinessUserItem: Using stored data for user {$user->id} ({$this->date->month}/{$this->date->year})");
// WICHTIG: Auch bei gespeicherten Daten User-Grunddaten anreichern
$this->enrichStoredDataWithUserModel($user);
@ -134,19 +139,18 @@ class BusinessUserItemOptimized
return; // Bereits berechnete Daten verwenden
}
} else {
\Log::debug("BusinessUserItem: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})");
\Log::debug("BusinessUserItemOptimized: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})");
}
// Erstelle neuen User und führe Live-Berechnung durch
$this->initializeFromUserModel($user);
// WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen
// (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden)
if ($forceLiveCalculation) {
$this->calcQualPP();
//$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user from model {$user->id}: " . $e->getMessage());
\Log::error("BusinessUserItemOptimized: Error creating user from model {$user->id}: " . $e->getMessage());
throw $e;
}
}
@ -204,6 +208,9 @@ class BusinessUserItemOptimized
'qual_kp' => $user_level_active ? max(0, $user_level_active->qual_kp) : 0,
'qual_pp' => $user_level_active ? max(0, $user_level_active->qual_pp) : 0,
'active_growth_bonus' => $user_level_active ? (float)$user_level_active->growth_bonus : 0,
'growth_bonus_details' => null,
// Initialisierung
'payline_points' => 0,
'commission_pp_total' => 0,
@ -223,6 +230,7 @@ class BusinessUserItemOptimized
$this->b_user->commission_shop_sales = $calculatedCommission;
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year} - Shop commission: {$calculatedCommission} (Volume: {$shopVolume}, Margin: {$shopMargin}%)");
\Log::debug("BusinessUserItemOptimized: b_user: " . json_encode($this->b_user));
}
/**
@ -428,6 +436,24 @@ class BusinessUserItemOptimized
$this->b_user->business_lines[$line] = $obj;
}
/**
* Initialisiert leere business_lines für diesen User
*/
public function initBusinessLines(): void
{
if (!isset($this->b_user->business_lines) || !is_array($this->b_user->business_lines)) {
$this->b_user->business_lines = [];
}
}
/**
* Prüft ob eine business_line existiert
*/
public function hasBusinessLine(int $line): bool
{
return isset($this->b_user->business_lines[$line]);
}
public function addBusinessLinePoints($line, $points)
{
if (!isset($this->b_user->business_lines[$line])) {
@ -451,6 +477,78 @@ class BusinessUserItemOptimized
$this->b_user->business_lines[$line] = $obj;
}
/**
* Gibt Details zur Growth Bonus Berechnung zurück (für die View)
* Nur für Monate ab November 2025 verfügbar (neue Logik)
*/
public function getGrowthBonusBreakdown(): array
{
// Prüfe ob Legacy-Monat (vor November 2025)
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
return [];
}
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
return [];
}
try {
$calculator = new GrowthBonusCalculator();
// Array zu Object konvertieren für Calculator
$qualData = (object) $this->b_user->qual_user_level;
return $calculator->getCalculationDetails($this, $qualData);
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting growth bonus breakdown: " . $e->getMessage());
return [];
}
}
/**
* Gibt Matrix-Details zur Growth Bonus Berechnung zurück (für die View)
* Nur für Monate ab November 2025 verfügbar (neue Logik)
*/
public function getGrowthBonusMatrix(): array
{
// Prüfe ob Legacy-Monat (vor November 2025)
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
return [];
}
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
return [];
}
// Use stored details if available (avoid recalculation)
if (!empty($this->b_user->growth_bonus_details)) {
if (is_object($this->b_user->growth_bonus_details) && method_exists($this->b_user->growth_bonus_details, 'toArray')) {
return $this->b_user->growth_bonus_details->toArray();
}
if (is_array($this->b_user->growth_bonus_details)) {
return $this->b_user->growth_bonus_details;
}
// Fallback for standard object
if (is_object($this->b_user->growth_bonus_details)) {
return json_decode(json_encode($this->b_user->growth_bonus_details), true);
}
}
try {
$calculator = new GrowthBonusCalculator();
// Array zu Object konvertieren für Calculator
$qualData = (object) $this->b_user->qual_user_level;
return $calculator->getMatrixDetails($this, $qualData);
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting growth bonus matrix: " . $e->getMessage());
return [];
}
}
public function addTotalTP($points)
{
$this->b_user->total_pp += (float) $points; // Type-Safety
@ -466,6 +564,67 @@ class BusinessUserItemOptimized
return !empty($this->b_user->qual_user_level);
}
/**
* Methode für Zugriff auf qual_user_level (auch für GrowthBonusCalculator)
*/
public function getQualUserLevel()
{
return $this->b_user->qual_user_level ?? null;
}
public function getActiveGrowthBonus()
{
return $this->active_growth_bonus;
}
/**
* Gibt das Date-Objekt zurück (für GrowthBonusCalculator)
*/
public function getDate()
{
return $this->date;
}
/**
* Gibt den Growth Bonus basierend auf dem ERREICHTEN Qualifikations-Level zurück.
*
* WICHTIG: Diese Methode gibt den Growth Bonus nur zurück, wenn der Partner
* in dem Monat tatsächlich das entsprechende Level qualifiziert hat.
* Das ist entscheidend für die korrekte Differenz-Berechnung im GrowthBonusCalculator.
*
* Die Methode funktioniert sowohl für:
* - Live-berechnete Daten (qualificationCalculated = true)
* - Gespeicherte/geladene Daten aus UserBusiness (qual_user_level bereits vorhanden)
*
* @return float Der Growth Bonus des erreichten Qualifikations-Levels (0 wenn nicht qualifiziert)
*/
public function getQualifiedGrowthBonus(): float
{
// Prüfen ob b_user existiert
if (empty($this->b_user)) {
return 0.0;
}
// Prüfen ob ein Qualifikations-Level erreicht wurde
// Dies funktioniert sowohl für live-berechnete als auch für gespeicherte Daten
if (empty($this->b_user->qual_user_level)) {
return 0.0;
}
// Handle array und object Zugriff (JSON-Deserialisierung kann beides liefern)
$qualLevel = $this->b_user->qual_user_level;
if (is_array($qualLevel)) {
return (float) ($qualLevel['growth_bonus'] ?? 0.0);
}
if (is_object($qualLevel)) {
return (float) ($qualLevel->growth_bonus ?? 0.0);
}
return 0.0;
}
public function isQualEqualLevel(): bool
{
if (!$this->b_user->qual_user_level) {
@ -502,11 +661,26 @@ class BusinessUserItemOptimized
public function calcQualPP($force = false): void
{
if ($this->qualificationCalculated && !$force) {
return;
}
// Mark as calculated immediately to prevent potential recursion loops
$this->qualificationCalculated = true;
try {
$qualUserLevel = $this->calcuQualLevel();
\Log::debug("BusinessUserItemOptimized: calcQualPP for user {$this->b_user->user_id}: " . json_encode($qualUserLevel));
if ($qualUserLevel !== null) {
//das erreichte level setzen
$this->b_user->qual_user_level = $qualUserLevel->toArray();
// Wichtig: Setze die qual_kp und qual_pp des erreichten Levels im b_user Objekt
// Diese Werte ändern sich je nach erreichtem Level und müssen hier aktualisiert werden
$this->b_user->qual_kp = $qualUserLevel->qual_kp;
$this->b_user->qual_pp = $qualUserLevel->qual_pp;
\Log::debug("BusinessUserItemOptimized: Set qual_kp={$qualUserLevel->qual_kp}, qual_pp={$qualUserLevel->qual_pp} for user {$this->b_user->user_id}");
//next_qual_user_level nächster qualifizierten level
$this->setNextUserLevel($force);
//qual_user_level_next nächste Provisions-Stufe,
@ -557,15 +731,57 @@ class BusinessUserItemOptimized
// Growth Bonus
if (!empty($qualUserLevel->growth_bonus)) {
$payline = (int) $this->b_user->qual_user_level['paylines'] + 1;
$maxlines = count($this->b_user->business_lines) + 1;
$growth_bonus = (float) $this->b_user->qual_user_level['growth_bonus'];
// Fallback für alte Monate (vor November 2025)
// Stichtag: 01.11.2025 - Alles davor nutzt die Legacy-Berechnung
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
$commission_growth_total = $this->calculateLegacyGrowthBonus($qualUserLevel);
\Log::debug("BusinessUserItem: Used LEGACY growth bonus calculation for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})");
} else {
// Neue Logik ab Dezember 2025 - delegated to new Calculator service
try {
$growthCalculator = new GrowthBonusCalculator();
$commission_growth_total = $growthCalculator->calculate($this, $qualUserLevel);
// Calculate matrix details for storage and total sum
// This ensures that the stored details match the calculated total exactly
$matrixDetails = $growthCalculator->getMatrixDetails($this, $qualUserLevel);
// Store details in the model so they can be retrieved later without recalculation
$this->b_user->growth_bonus_details = $matrixDetails;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error calculating growth bonus for user {$this->b_user->user_id}: " . $e->getMessage());
// Fallback to 0 if calculation fails
$commission_growth_total = 0;
$this->b_user->growth_bonus_details = null;
}
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
}
/**
* Alte Berechnungsmethode für Growth Bonus (Kompatibilität für vergangene Monate)
* Berechnet pauschal ab einer bestimmten Ebene ohne Differenz-Prüfung
*/
private function calculateLegacyGrowthBonus($qualUserLevel): float
{
$commission_growth_total = 0;
// Payline aus Level-Daten + 1 (Start des Bonus)
$payline = (int) ($this->b_user->qual_user_level['paylines'] ?? 0) + 1;
$maxlines = count($this->b_user->business_lines ?? []) + 1;
$growth_bonus = (float) ($this->b_user->qual_user_level['growth_bonus'] ?? 0);
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
// Handle both array and object types (JSON deserialization inconsistency)
// Handle both array and object types
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
$object['margin'] = $growth_bonus;
@ -573,6 +789,9 @@ class BusinessUserItemOptimized
$object['growth_bonus'] = true;
$commission_growth_total += $object['commission'];
} else {
if (!is_object($object)) {
$object = (object) $object;
}
$points = (float) ($object->points ?? 0);
$object->margin = $growth_bonus;
$object->commission = round($points / 100 * $growth_bonus, 2);
@ -583,36 +802,58 @@ class BusinessUserItemOptimized
$this->b_user->business_lines[$i] = $object;
}
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
return $commission_growth_total;
}
// ===== WEITERE ORIGINAL-METHODEN (gekürzt, vollständige Implementation in Original) =====
//aktuelles level berechnen, max das eigene level, wenn weniger Points dann darunter
/**
* Berechnet das aktuell erreichte Level
* Durchläuft alle möglichen Levels (max. bis zur eigenen User-Level-Position)
* und prüft dynamisch die Qualifikation basierend auf den spezifischen qual_kp und qual_pp des jeweiligen Levels
*/
public function calcuQualLevel()
{
\Log::debug("BusinessUserItemOptimized: calcuQualLevel for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})");
// Hole alle möglichen Levels bis zur eigenen Position, sortiert nach Position absteigend
// um vom höchsten zum niedrigsten zu prüfen
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->b_user->sales_volume_points_KP_sum)
->where('pos', '<=', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
->orderBy('pos', 'desc') // Sortiere nach Position DESC, um das höchste Level zuerst zu prüfen
->get();
foreach ($qualUserLevels as $qualUserLevel) {
// Berechne die Payline-Punkte für die spezifischen Paylines dieses Levels
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
\Log::debug("BusinessUserItemOptimized: payline_points: " . $payline_points);
// WICHTIG: Berechne die Rest-KP basierend auf der qual_kp DES AKTUELL GEPRÜFTEN LEVELS
// nicht der qual_kp des bereits gesetzten Levels (das war der Fehler!)
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevel->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Prüfe ob die Qualifikation für diesen spezifischen Level erfüllt ist
if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) {
// Setze die berechneten Werte
$this->b_user->calc_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$this->b_user->payline_points = $payline_points;
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
$qualUserLevel->_calculated_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$qualUserLevel->_calculated_payline_points = $payline_points;
$qualUserLevel->_calculated_payline_points_qual_kp = $payline_points_qual_kp;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for level {$qualUserLevel->name} (pos: {$qualUserLevel->pos}) - Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$qualUserLevel->qual_pp}");
return $qualUserLevel;
}
}
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not qualify for any level");
return null;
}
private function getPointsforPayline($paylines): float
{
\Log::debug("BusinessUserItemOptimized: getPointsforPayline for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year}) with paylines: " . $paylines . " and business_lines: " . json_encode($this->b_user->business_lines));
$payline_points = 0;
for ($i = 1; $i <= $paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
@ -642,7 +883,18 @@ class BusinessUserItemOptimized
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
// Berechne die spezifischen Werte für diesen Level
$payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines);
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Speichere Level-Daten mit berechneten Werten
$levelData = $qualUserLevelNext->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
$this->b_user->qual_user_level_next = $levelData;
} else {
$this->b_user->qual_user_level_next = null;
}
@ -653,24 +905,50 @@ class BusinessUserItemOptimized
private function setNextUserLevel($force = false): void
{
//sucht den nächsten level, der mehr points hat als das aktuelle level
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->b_user->payline_points_qual_kp)
->where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
// Hole nur den direkt nächsten Level (keine Level überspringen!)
$nextLevel = UserLevel::where('pos', '=', $this->user_level_active_pos + 1)
->first();
//wenn der nächste level qualifiziert ist und die KP-Qualifikation erfüllt ist, dann setzt es den nächsten level
if ($nextQualUserLevel && $this->isQualKP()) {
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
$this->b_user->next_can_user_level = null;
} else {
//wenn der nächste level nicht qualifiziert ist, dann sucht es den nächsten level, nach pos
$nextCanUserLevel = UserLevel::where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'asc')
->first();
if ($nextCanUserLevel) {
$this->b_user->next_can_user_level = $nextCanUserLevel->toArray();
}
// Wenn kein nächster Level existiert, beende
if (!$nextLevel) {
$this->b_user->next_qual_user_level = null;
$this->b_user->next_can_user_level = null;
\Log::debug("BusinessUserItemOptimized: No next level found for user {$this->b_user->user_id} (already at highest level)");
return;
}
// Berechne die Payline-Punkte für die spezifischen Paylines des nächsten Levels
$payline_points = $this->getPointsforPayline($nextLevel->paylines);
// Berechne die Rest-KP basierend auf dem nächsten Level
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $nextLevel->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Erstelle Level-Daten mit berechneten Werten
$levelData = $nextLevel->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $nextLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
// Prüfe die KP-Qualifikation für den nächsten Level
if ($this->b_user->sales_volume_points_KP_sum < $nextLevel->qual_kp) {
// KP-Qualifikation nicht erfüllt - zeige als "next_can_user_level"
$this->b_user->next_can_user_level = $levelData;
$this->b_user->next_qual_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet KP requirement for next level {$nextLevel->name} ({$this->b_user->sales_volume_points_KP_sum} < {$nextLevel->qual_kp})");
return;
}
// Prüfe ob die PP-Qualifikation erfüllt ist
if ($payline_points_qual_kp >= $nextLevel->qual_pp) {
// Qualifiziert für den nächsten Level
$this->b_user->next_qual_user_level = $levelData;
$this->b_user->next_can_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for next level {$nextLevel->name} (Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$nextLevel->qual_pp})");
} else {
// PP-Qualifikation nicht erfüllt - zeige als "next_can_user_level"
$this->b_user->next_can_user_level = $levelData;
$this->b_user->next_qual_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet PP requirement for next level {$nextLevel->name} ({$payline_points_qual_kp} < {$nextLevel->qual_pp})");
}
}
@ -680,7 +958,15 @@ class BusinessUserItemOptimized
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
$payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines);
// Berechne die Rest-KP basierend auf dem nächsten Level
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
$levelData = $qualUserLevelNext->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
$this->b_user->qual_user_level_next = $levelData;
}
}

View file

@ -0,0 +1,136 @@
# Ergänzung: Block-Status Erkennung
Das Problem ist aktuell, dass wir zwar den `growth_bonus` aus dem Qualifikations-Level auslesen, aber nicht explizit wissen, ob dieser User für den Upline-Partner als "Blocker" gilt oder nicht.
## Die Logik:
Ein User gilt als "Blocker" (bzw. er beansprucht Schutz), wenn:
1. Er ein Level erreicht hat (`isQualLevel()`).
2. Dieses Level einen `growth_bonus > 0` hat.
Das ist bereits in `GrowthBonusCalculator::getVolumeByProtectionLevel` und `collectLegLevels` implementiert:
```php
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growthBonus = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growthBonus > 0) {
$myProtectionPercent = (float) $growthBonus;
}
}
```
## Was fehlt / optimiert werden muss:
Wenn du sagst "User die einen Level haben wo tiefebonus und der level auch erreicht ist ... wird der bonus geblockt oder geteilt", dann meinst du wahrscheinlich, dass wir sicherstellen müssen, dass `qual_user_level` korrekt gesetzt ist **UND** wir diesen Status auch in der Matrix-Ansicht korrekt als "Block" visualisieren.
Aktuell zeigt die Matrix "Block (0%)" nur an, wenn die rechnerische Differenz <= 0 ist.
Es wäre hilfreich, in der Matrix anzuzeigen, **WARUM** blockiert wird (z.B. "Blocked by Silver").
### Anpassung in `GrowthBonusCalculator::getMatrixDetails` (bzw. `collectLegLevels`)
Wir erweitern die Detail-Informationen in der Matrix um den Status des Users, der den Schutz erhöht hat.
In `collectLegLevels`:
Wir müssen erkennen, ob der aktuelle User den Schutz erhöht hat.
```php
// Protection Check
$userProtection = 0.0;
$blockerLevelName = null;
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$userProtection = (float) $growth;
$blockerLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
}
}
// Hat dieser User den Schutz erhöht?
$isBlocker = $userProtection > $incomingProtection;
$effectiveProtection = max($incomingProtection, $userProtection);
```
Diese Info speichern wir in `$legData['levels'][$level]`.
## Code-Anpassung
Ich werde `GrowthBonusCalculator.php` anpassen, um diese Meta-Daten zu erfassen, damit wir sie in der View anzeigen können.
### Schritt 1: `GrowthBonusCalculator.php` - `collectLegLevels` erweitern
```php
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
{
// 1. Eigenen Status ermitteln
$userProtection = 0.0;
$userLevelName = '';
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$userProtection = (float) $growth;
$userLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
}
}
// Berechnung
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($volume > 0) {
$diffPercent = max(0, $myPercent - $incomingProtection);
$commission = round($volume / 100 * $diffPercent, 2);
if (!isset($legData['levels'][$level])) {
$legData['levels'][$level] = [
'volume' => 0.0,
'commission' => 0.0,
'details' => []
];
}
$legData['levels'][$level]['volume'] += $volume;
$legData['levels'][$level]['commission'] += $commission;
// Erweiterte Details speichern
$legData['levels'][$level]['details'][] = [
'u' => $item->user_id,
'name' => $item->first_name . ' ' . $item->last_name,
'level' => $userLevelName, // Welchen Status hat dieser User?
'prot_own' => $userProtection, // Welchen Schutz baut er selbst auf?
'prot_in' => $incomingProtection, // Welcher Schutz kam von oben?
'percent' => $diffPercent
];
$legData['total_volume'] += $volume;
$legData['total_commission'] += $commission;
}
// Protection für nächste Ebene
$nextProtection = max($incomingProtection, $userProtection);
// Rekursion ...
}
```
### Schritt 2: View Update
In der View können wir nun anzeigen, wenn ein User auf einer Ebene einen Status hat (Tooltip oder Icon).
Da wir in der Matrix pro Ebene aggregieren (falls ein Bein sich verzweigt, was hier in `collectLegLevels` durch die Rekursion über `businessUserItems` passiert - Moment, ein "Bein" ist hier linear in der Tiefe, aber in jeder Tiefe kann es Breite geben).
Warte, `collectLegLevels` geht `foreach ($item->businessUserItems as $child)`.
Das bedeutet, ein "Leg" in der Matrix ist eigentlich ein ganzer Unterbaum.
In Ebene 2 können also mehrere User sein (alle Enkel in diesem Bein).
Wenn wir in der Matrix nur EINE Zelle pro Ebene haben, müssen wir entscheiden, was wir anzeigen.
Die Summe (€/Volumen) ist korrekt.
Aber der Status ("Block") kann unterschiedlich sein (ein Enkel ist Silber, der andere nicht).
Lösung: Wir markieren die Zelle als "Teilweise Blockiert" oder zeigen Details im Tooltip an.
Ich passe zuerst den `GrowthBonusCalculator` an, um diese Daten bereitzustellen.

View file

@ -0,0 +1,193 @@
# Konzept: Erweiterte Detailansicht für Growth Bonus (Matrix-View)
Die Anforderung ist, eine **Matrix-Ansicht** zu erstellen, bei der die Ebenen (Level 1, 2, 3...) als Spalten und die einzelnen Linien (Legs/Beine) als Zeilen dargestellt werden. Dies soll auch dann geschehen, wenn der Bonus gekappt ist, um volle Transparenz zu gewährleisten.
## 1. Datenstruktur-Erweiterung (`GrowthBonusCalculator`)
Die bisherige Aggregation (`getVolumeByProtectionLevel`) gruppiert Volumen nach "Schutz-Level". Das ist gut für die Berechnung, aber für die Visualisierung "Ebene für Ebene" brauchen wir die Rohdaten pro Ebene.
Wir benötigen eine neue Methode `getMatrixDetails`, die rekursiv die Struktur traversiert und für jedes Bein eine flache Liste von Ebenen-Volumen zurückgibt, angereichert mit Status-Informationen.
### Struktur des Ergebnis-Arrays:
```php
[
// Ein Eintrag pro Firstline (Bein)
[
'user' => [ 'id' => 123, 'name' => 'Max Mustermann', 'level' => 'Gold' ],
'levels' => [
1 => [ // Ebene 1 (relativ zu mir, also der Firstline-User selbst)
'volume' => 500,
'user_level' => 'Gold',
'protection_percent' => 2.0, // Was der User für sich beansprucht
'my_percent' => 2.5, // Mein Anspruch
'diff_percent' => 0.5, // Resultierende Provision
'commission' => 2.50,
'is_blocked' => false
],
2 => [ // Ebene 2 (User unter Max)
'volume' => 1000,
'user_level' => 'Silver',
'protection_percent' => 1.5,
'my_percent' => 2.5,
'diff_percent' => 1.0,
'commission' => 10.00,
'is_blocked' => false
],
// ... weitere Ebenen bis max Tiefe oder Abbruchbedingung
],
'totals' => [ 'volume' => 1500, 'commission' => 12.50 ]
],
// ... weitere Beine
]
```
## 2. Implementierungsschritte
1. **`GrowthBonusCalculator.php`**: Methode `getMatrixDetails` hinzufügen.
* Muss rekursiv durch die `businessUserItems` laufen.
* Muss tracken, welcher "Schutz-Level" von oben kommt (rekursiv weitergegeben).
* Muss aber `protection_percent` lokal pro User neu bewerten (max(incoming, own)).
2. **`BusinessUserItemOptimized.php`**: Aufruf in `getGrowthBonusBreakdown` anpassen oder neue Methode `getGrowthBonusMatrix` hinzufügen.
3. **View `_user_detail_in.blade.php`**: Umbau der Tabelle zu einer Matrix.
### Herausforderung: Tiefe und Breite
Eine komplette Matrix kann sehr breit und lang werden.
* **Begrenzung:** Wir sollten die Tiefe standardmäßig begrenzen (z.B. 10-20 Ebenen) oder nur relevante Ebenen (wo Volumen > 0) anzeigen.
* **Breite:** In der Tabelle werden die Spalten "Ebene 1", "Ebene 2", ... sein.
## 3. Code-Anpassung `GrowthBonusCalculator.php`
```php
/**
* Liefert eine Matrix-Sicht für die detaillierte Darstellung
* Zeilen = Beine (Legs), Spalten = Ebenen (Levels)
*/
public function getMatrixDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
{
$details = [];
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return $details;
}
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
foreach ($userItem->businessUserItems as $childItem) {
$legData = [
'user' => [
'id' => $childItem->user_id,
'name' => $childItem->first_name . ' ' . $childItem->last_name,
'level' => $childItem->user_level_name
],
'levels' => [],
'total_commission' => 0.0,
'total_volume' => 0.0
];
// Rekursiv die Ebenen dieses Beins einsammeln
// Start bei Ebene 1 (das ist das Kind selbst)
// Initial Protection ist 0 (vom Upline/Mir kommt kein Schutz, der relevant wäre, da ICH ja der Empfänger bin)
$this->collectLegLevels($childItem, 1, 0.0, $myGrowthPercent, $legData);
if (!empty($legData['levels'])) {
// Sortieren nach Ebenen-Index
ksort($legData['levels']);
$details[] = $legData;
}
}
// Sortieren nach Gesamt-Provision
usort($details, function($a, $b) {
return $b['total_commission'] <=> $a['total_commission'];
});
return $details;
}
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
{
// 1. Eigenen Status ermitteln (Schutz für Downline)
$myProtection = 0.0;
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$myProtection = (float) $growth;
}
}
// Der effektive Schutz, der AUF diesen User wirkt (von oben kommend + sein eigener Anspruch)
// WICHTIG: Für die Provision auf DIESEN User zählt der $incomingProtection (Schutz von oben).
// Für die Weitergabe nach unten zählt max($incoming, $myProtection).
// Berechnung für diesen User (Ebene)
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($volume > 0) {
// Differenz: Mein Anspruch - Schutz von oben
$diffPercent = max(0, $myPercent - $incomingProtection);
$commission = round($volume / 100 * $diffPercent, 2);
// Speichern in Matrix
// Wir summieren Volumen pro Ebene (falls durch parallele Zweige im Bein mehrere User auf gleicher Ebene sind - hier aber linearer Abstieg)
// Moment, businessUserItems ist ein Baum. Ein Bein kann breit werden.
// Wir müssen pro Ebene summieren.
if (!isset($legData['levels'][$level])) {
$legData['levels'][$level] = [
'volume' => 0.0,
'commission' => 0.0,
'details' => [] // Optional für Hover
];
}
$legData['levels'][$level]['volume'] += $volume;
$legData['levels'][$level]['commission'] += $commission;
// Metadaten für Anzeige (nur beim ersten Eintrag pro Ebene oder aggregiert?)
// Bei Matrix-View (Spalten=Ebenen) summieren wir alles auf Ebene X in diesem Bein.
// Das "Problem": In Ebene X können User mit unterschiedlichem Schutz-Status sein.
// Daher ist eine einfache Summe evtl. irreführend bei der %-Anzeige.
// Alternative: Wir zeigen pro Ebene den "dominanten" Status oder listen auf.
// Für die Tabelle ist eine Zelle pro Ebene vorgesehen.
// Wir speichern Detail-Infos für Tooltip.
$legData['levels'][$level]['details'][] = [
'u' => $item->user_id,
'v' => $volume,
'p' => $incomingProtection, // Protected by
'd' => $diffPercent
];
$legData['total_volume'] += $volume;
$legData['total_commission'] += $commission;
}
// Protection für nächste Ebene: Maximum aus was von oben kam und was dieser User beansprucht
$nextProtection = max($incomingProtection, $myProtection);
// Rekursion
// Max Tiefe z.B. 20
if ($level < 20 && !empty($item->businessUserItems)) {
foreach ($item->businessUserItems as $child) {
$this->collectLegLevels($child, $level + 1, $nextProtection, $myPercent, $legData);
}
}
}
```
## 4. Design der Tabelle (Blade)
Spalten: Leg (Partner) | Ebene 1 | Ebene 2 | Ebene 3 | ... | Ebene 10 | Total
Zeilen: Partner A | ... | ... | ...
Zellen-Inhalt:
* Oben: Provision (€)
* Unten: Volumen (Pkt)
* Farbe: Grün (Volle %), Gelb (Teil %), Rot (0% / Block)
Da die Ebenen dynamisch sind, ermitteln wir `max_level` über alle Legs.

View file

@ -0,0 +1,399 @@
# Funktionsweise: Tiefenbonus (Growth Bonus)
## ⚠️ WICHTIG: Bug-Fix November 2025
### Das Problem (vor November 2025)
Die Payline-Prozentsätze (`pr_line_1` bis `pr_line_6`) in der Datenbank enthielten **bereits den Growth Bonus**.
**Beispiel Gold Member (falsche Berechnung):**
| Ebene | Wert in DB (`pr_line_X`) | Was ausgezahlt wurde | Was korrekt gewesen wäre |
| ------- | ------------------------ | -------------------- | ------------------------------ |
| Ebene 1 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 2 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 3 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 4 | 6% | 6% | 4% Payline + 2% Growth = 6% |
| Ebene 5 | 4% | 4% | 2% Payline + 2% Growth = 4% |
| Ebene 6 | 4% | 4% | 2% Payline + 2% Growth = 4% |
| Ebene 7 | - | 2% (Growth nochmal!) | 2% Growth (nur mit Differenz!) |
**Problem:** Der Growth Bonus wurde **doppelt gezählt**:
1. Einmal IN den Payline-Prozentsätzen (pr_line_1 = 9% statt 7%)
2. Nochmal SEPARAT auf Ebenen ab 7+ (Legacy-Berechnung)
### Die Lösung (ab November 2025)
1. **Payline-Prozentsätze korrigiert:** `pr_line_X` enthält NUR den Payline-Anteil
2. **Growth Bonus separat:** Wird mit Differenz-Logik berechnet
3. **Einmal pro Bein:** Growth Bonus wird nur EINMAL pro Firstline-Zweig ausgezahlt
**Beispiel Gold Member (korrekte Berechnung):**
| Ebene | Payline (`pr_line_X`) | Growth Bonus (separat) | Gesamt |
| -------- | --------------------- | ---------------------- | ------ |
| Ebene 1 | 7% | +2% (Differenz-Logik) | 9% |
| Ebene 2 | 7% | +2% | 9% |
| Ebene 3 | 7% | +2% | 9% |
| Ebene 4 | 4% | +2% | 6% |
| Ebene 5 | 2% | +2% | 4% |
| Ebene 6 | 2% | +2% | 4% |
| Ebene 7+ | - | +2% (Differenz-Logik) | 2% |
**Wichtig:** Der Growth Bonus wird NUR ausgezahlt, wenn kein gleichrangiger oder höherer Partner in der Downline ist (Differenz-Berechnung)!
---
## Differenz-Logik (ab November 2025)
Der Tiefenbonus ist ein **Differenz-Bonus**, der **sofort ab der 1. Ebene** beginnt.
Es gilt das Prinzip: **"Jeder Partner schützt sein eigenes Team-Volumen."**
### 1. Die Grundregel
- **Start:** Der Bonus berechnet sich auf Points ab der **1. Ebene** (direkte Downline).
- **Anspruch:** Ein Partner erhält seinen Status-Prozentsatz auf alle Points in seiner Linie, **bis** er auf einen Partner trifft, der selbst einen Status-Anspruch hat.
- **Blockade:** Sobald ein Partner in der Downline einen Anspruch hat, zieht er diesen von der Upline ab (Differenz-Rechnung).
- **⚠️ WICHTIG - Erreichtes Qualifikations-Level:** Die Blockade erfolgt NUR basierend auf dem **in dem Monat tatsächlich erreichten Level** (`qual_user_level`), NICHT auf dem aktuellen Karriere-Level des Partners!
### 1.1 Erreichte Qualifikation vs. Aktuelles Level
Ein Partner kann ein bestimmtes Karriere-Level (z.B. Gold) haben, aber in einem Monat die Qualifikationsvoraussetzungen nicht erfüllen. In diesem Fall:
| Situation | Aktuelles Level | Erreicht in Monat | Blockiert mit |
| --------- | --------------- | ----------------- | ------------- |
| Fall A | Gold (2%) | Gold qualifiziert | 2% ✅ |
| Fall B | Gold (2%) | Team Leader (0%) | 0% ❌ |
| Fall C | Team Leader | Silber (1.5%) | 1.5% ✅ |
**Technische Umsetzung:**
- Die Methode `getQualifiedGrowthBonus()` in `BusinessUserItemOptimized` gibt den Growth Bonus basierend auf dem **erreichten Qualifikations-Level** (`qual_user_level`) zurück.
- Die alte Methode `getActiveGrowthBonus()` gibt den Growth Bonus basierend auf dem **aktuellen Karriere-Level** zurück (NUR für Legacy-Berechnungen!).
- Der `GrowthBonusCalculator` verwendet ab November 2025 ausschließlich `getQualifiedGrowthBonus()`.
---
### 2. Die Differenz (Der Normalfall)
Points entstehen irgendwo im Team von **Partner B** (egal ob in B's Ebene 1 oder B's Ebene 50).
**Die Verteilung:**
1. **Sicht Partner B (Silber):**
- Er hat Anspruch auf **1,5 %** auf sein gesamtes Team.
- Da unter ihm (Partner C) niemand einen Status hat, der etwas wegnehmen könnte, erhält B die vollen **1,5 %**.
- Damit sind 1,5 % des "Kuchens" verteilt.
2. **Sicht Partner A (Diamant):**
- Du hast Anspruch auf **2,5 %**.
- Du schaust auf die Linie von Partner B.
- Partner B hat den Status Silber und beansprucht damit **1,5 %** für sich und sein ganzes Team.
- **Deine Rechnung:** 2,5 % (Dein Anspruch) - 1,5 % (Anspruch B) = **1,0 %**.
- **Ergebnis:** Du erhältst auf das gesamte Volumen unter Partner B exakt **1,0 %**.
---
### 2. Das "GAP" (Die direkte Ebene)
Da der Bonus ab Ebene 1 beginnt, entsteht das GAP (die Auszahlung trotz gleichem Rang) immer am **Eigenumsatz des Partners**:
- **Partner A** (Diamant, 2,5 %) ist Sponsor von **Partner B** (Diamant, 2,5 %).
- **Punkte von B (Eigenbestellung/Kunden):**
- Partner B erhält darauf _keinen_ Tiefenbonus (man kriegt keinen Tiefenbonus auf sich selbst).
- Partner B zieht also **0 %** vom Topf ab.
- **Partner A erhält die vollen 2,5 % auf die Punkte von B.**
- **Punkte UNTER B (Team von B):**
- Partner B greift hier zu (Start ab Ebene 1) und nimmt sich **2,5 %**.
- Partner A rechnet: 2,5 % - 2,5 % = **0 %**.
- **Partner A ist hier blockiert.**
> Fazit: Bei gleichem Rang verdient man nur an den direkten Points des Partners (GAP), aber nicht mehr an dessen Team.
---
### 3. Das Szenario (A -> B -> F)
Wir schauen uns deine Struktur mit 3 Diamanten in einer Linie an. Alle haben Anspruch auf **2,5 %**.
- **Partner A** (Ebene 1)
- **Partner B** (Ebene 2, direkt unter A)
- ... dazwischen Berater ohne Status ...
- **Partner F** (Ebene 6, unter B)
- ... Punkte entstehen unter F ...
### Bereich 1: Punkte von Partner B
- Das ist für **A** die Ebene 1.
- B blockiert nicht (da Eigenumsatz).
- **Ergebnis:** **A erhält 2,5 %**.
### Bereich 2: Punkte ZWISCHEN B und F (Ebene 3 bis 6)
- Hier entstehen Punkte im Team von B.
- **Sicht B:** Er ist qualifiziert (Start ab Ebene 1). Er nimmt sich **2,5 %**.
- **Sicht A:** Er hat Anspruch auf 2,5 %. B hat aber schon 2,5 % genommen. Differenz = 0 %.
- **Ergebnis:** **B erhält 2,5 %**. A geht leer aus.
### Bereich 3: Punkte von Partner F
- Das ist für **B** eine Ebene in seiner Downline.
- F blockiert hier noch nicht (Eigenumsatz).
- **Ergebnis:** **B erhält 2,5 %** auf die Punkte von F.
### Bereich 4: Punkte UNTER F (ab Ebene 7)
- Hier entstehen Punkte im Team von F.
- **Sicht F:** Er ist qualifiziert (Start ab Ebene 1). Er nimmt sich **2,5 %**.
- **Sicht B:** Anspruch 2,5 %. F hat schon 2,5 % genommen. Differenz = 0 %.
- **Sicht A:** Anspruch 2,5 %. B (und F) haben alles genommen. Differenz = 0 %.
- **Ergebnis:** **F erhält 2,5 %**. B und A gehen leer aus.
---
### 4. Zusammenfassung für die IT-Logik
1. **Trigger:** Ein Umsatz (Points) entsteht bei User X.
2. **Schleife:** Gehe die Upline hoch (Sponsor -> Sponsor...).
3. **Prüfung:**
- Hat der Upline-Partner einen Status? (z.B. Diamant).
- (Keine Prüfung auf Ebene mehr nötig, da Start immer ab Ebene 1).
4. **Rechnung:**
- Auszahlung = Mein %-Satz - Bereits verteilter %-Satz.
- Wenn Auszahlung > 0: Speichern.
- Setze `Bereits verteilter %-Satz` auf den neuen Wert (also `Mein %-Satz`).
---
## Code-Implementierung
Diese Implementierung nutzt eine **rekursive Aggregation von Volumen nach "Schutz-Level"**.
Anstatt für jede Transaktion die Upline hochzulaufen ("Push"), holt sich der User die aggregierten Volumina seiner Downline gruppiert nach dem bereits beanspruchten Prozentsatz ("Pull").
### A. Neue Methode `getVolumeByProtectionLevel()`
Diese Methode liefert ein Array zurück, das das Volumen nach "bereits verteiltem Prozentsatz" gruppiert.
Format: `['0.0' => 1000, '1.5' => 5000, ...]`
```php
/**
* Liefert das Volumen der Downline gruppiert nach dem "bereits verteilten Prozentsatz" (Protection Level).
* Rekursive Funktion, die die "Differenz-Logik" vorbereitet.
*
* @return array<string, float> Key = Protected Percent, Value = Volume Points
*/
public function getVolumeByProtectionLevel(): array
{
$volumes = [];
// 1. Eigenes Volumen (Unprotected / GAP)
// Man selbst schützt seinen eigenen Umsatz NICHT vor der Upline.
// Daher Start mit Protection Level 0.0 (oder dem was von unten kommt, aber hier ist es ja Eigenumsatz)
// WICHTIG: Wir nutzen das Feld, das auch TreeCalcBot für die Punkte nutzt
// sales_volume_points_TP_sum scheint in der DB/Model Logik für das relevante Volumen zu stehen
$ownVolume = (float) ($this->b_user->sales_volume_points_TP_sum ?? 0);
if ($ownVolume > 0) {
$key = '0.0';
if (!isset($volumes[$key])) $volumes[$key] = 0.0;
$volumes[$key] += $ownVolume;
}
// 2. Mein Schutz-Level ermitteln
// Das ist der Prozentsatz, den ICH auf mein Team beanspruche.
// Alles Volumen, das durch MICH hindurch zur Upline fließt, hat mindestens diesen Schutz-Level.
$myProtectionPercent = 0.0;
if ($this->isQualLevel()) {
$qual = $this->b_user->qual_user_level;
if (!empty($qual['growth_bonus'])) {
$myProtectionPercent = (float) $qual['growth_bonus'];
}
}
// 3. Kinder verarbeiten
if (!empty($this->businessUserItems)) {
foreach ($this->businessUserItems as $childItem) {
// Rekursion: Hol dir die Volumen-Töpfe aus der Downline
// Hinweis: Hier muss sichergestellt sein, dass die Kinder geladen sind.
// initBusinesslUserDetail lädt normalerweise die Struktur.
// Falls Kinder nicht geladen sind, müssten sie hier theoretisch geladen werden.
// Wir gehen davon aus, dass die Struktur bereits rekursiv via readParentsBusinessUsers geladen wurde.
$childVolumes = $childItem->getVolumeByProtectionLevel();
// 4. Schutz-Level anwenden (Aggregation)
foreach ($childVolumes as $protectedPercentStr => $vol) {
$incomingProtection = (float) $protectedPercentStr;
// Das Volumen ist bereits mit $incomingProtection geschützt.
// Da es nun durch MICH fließt, erhöht sich der Schutz auf MEINEN Level (falls meiner höher ist).
$effectiveProtection = max($incomingProtection, $myProtectionPercent);
$newKey = (string) $effectiveProtection;
if (!isset($volumes[$newKey])) $volumes[$newKey] = 0.0;
$volumes[$newKey] += $vol;
}
}
}
return $volumes;
}
```
### B. Neue Methode `calculateGrowthBonusRecursive()`
Diese Methode ersetzt die bisherige Berechnung und nutzt die oben definierte Aggregation.
```php
/**
* Berechnet den Growth Bonus (Tiefenbonus) basierend auf der Differenz-Logik.
*/
private function calculateGrowthBonusRecursive($qualUserLevel): float
{
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return 0.0;
}
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
$totalGrowthBonus = 0.0;
// Wir iterieren über alle direkten Beine (Firstlines)
foreach ($this->businessUserItems as $childItem) {
// Volumen-Verteilung aus diesem Bein abrufen
// Das Kind liefert uns: "Hier sind 1000 Punkte geschützt mit 0%, 5000 Punkte geschützt mit 1.5%"
$volumeDistribution = $childItem->getVolumeByProtectionLevel();
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
$alreadyDistributedPercent = (float) $protectedPercentStr;
// Differenz berechnen
// Mein Anspruch MINUS was schon verteilt wurde
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
if ($mySharePercent > 0) {
$commission = round($volume / 100 * $mySharePercent, 2);
$totalGrowthBonus += $commission;
// Optional Logging
// \Log::debug("Growth Bonus: User {$this->b_user->user_id} earns {$mySharePercent}% on {$volume} pts (Protected: {$alreadyDistributedPercent}%) from leg {$childItem->b_user->user_id}");
}
}
}
return $totalGrowthBonus;
}
```
### C. Integration in `calculateCommissions`
```php
private function calculateCommissions($qualUserLevel): void
{
$commission_pp_total = 0;
// 1. Normale Unilevel Provision (Payline) - NUR pr_line_X Werte
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
$margin = (float) $this->b_user->qual_user_level['pr_line_' . $i];
$points = is_array($object) ? ((float)($object['points'] ?? 0)) : ((float)($object->points ?? 0));
$commission = round($points / 100 * $margin, 2);
$commission_pp_total += $commission;
// Rückschreiben
if (is_array($object)) {
$object['margin'] = $margin;
$object['commission'] = $commission;
$object['payline'] = true;
} else {
$object->margin = $margin;
$object->commission = $commission;
$object->payline = true;
}
$this->b_user->business_lines[$i] = $object;
}
}
// 2. Growth Bonus - Unterscheidung Legacy vs. Neu
$commission_growth_total = 0;
if (!empty($qualUserLevel->growth_bonus)) {
// Stichtag: 01.11.2025
$isLegacy = ($this->date->year < 2025) ||
($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
// ALT: Pauschal ab Ebene paylines+1 (FALSCH - doppelte Auszahlung!)
$commission_growth_total = $this->calculateLegacyGrowthBonus($qualUserLevel);
} else {
// NEU: Differenz-Logik via GrowthBonusCalculator
$commission_growth_total = $this->calculateGrowthBonusRecursive($qualUserLevel);
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
}
```
---
## Legacy-Berechnung (vor November 2025) - DEPRECATED
**⚠️ Diese Logik war FALSCH und führte zu doppelter Auszahlung!**
```php
/**
* ALT: Pauschal Growth Bonus ab Ebene paylines+1
* PROBLEM: Growth Bonus war bereits in pr_line_X enthalten!
*/
private function calculateLegacyGrowthBonus($qualUserLevel): float
{
$commission_growth_total = 0;
// Start ab Ebene paylines+1 (z.B. 7 bei Gold)
$payline = (int) ($this->b_user->qual_user_level['paylines'] ?? 0) + 1;
$maxlines = count($this->b_user->business_lines ?? []) + 1;
$growth_bonus = (float) ($this->b_user->qual_user_level['growth_bonus'] ?? 0);
// Auf JEDE Ebene ab payline wird der volle Growth Bonus gezahlt
// OHNE Differenz-Prüfung = FALSCH!
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$points = $this->b_user->business_lines[$i]['points'] ?? 0;
$commission = round($points / 100 * $growth_bonus, 2);
$commission_growth_total += $commission;
}
}
return $commission_growth_total;
}
```
**Warum war das falsch?**
1. `pr_line_1` bei Gold = 9% (enthielt bereits 2% Growth Bonus)
2. Growth Bonus wurde ab Ebene 7 NOCHMAL mit 2% berechnet
3. = **Doppelte Auszahlung** auf tieferen Ebenen
---
## Neue Berechnung (ab November 2025) - KORREKT
Der `GrowthBonusCalculator` verwendet die Differenz-Logik:
1. **Aggregation:** Sammelt Volumen gruppiert nach "Schutz-Level"
2. **Differenz:** Berechnet nur die Differenz (mein Anspruch - bereits verteilt)
3. **Einmal pro Bein:** Growth Bonus wird nur einmal pro Firstline-Zweig ausgezahlt
Siehe `GrowthBonusCalculator.php` für die Implementation.

View file

@ -0,0 +1,381 @@
<?php
namespace App\Services\BusinessPlan;
use Illuminate\Support\Facades\Log;
/**
* Service für die Berechnung des Growth Bonus (Tiefenbonus)
* Implementiert die Differenz-Bonus-Logik ab Ebene 1
*/
class GrowthBonusCalculator
{
/**
* Berechnet den Growth Bonus für einen BusinessUserItemOptimized
*
* @param BusinessUserItemOptimized $userItem Der User, für den der Bonus berechnet wird
* @param object $qualUserLevel Das Qualifikations-Level-Objekt des Users
* @return float Der berechnete Bonus
*/
public function calculate(BusinessUserItemOptimized $userItem, $qualUserLevel): float
{
// Basis-Check: Hat der User überhaupt Anspruch auf Growth Bonus?
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return 0.0;
}
// Falls keine direkte Downline-Struktur geladen ist, kann kein Growth Bonus berechnet werden
if (empty($userItem->businessUserItems) && !empty($userItem->business_lines)) {
Log::warning("GrowthBonusCalculator: Growth Bonus calculation requires loaded child structure (businessUserItems is empty for user {$userItem->user_id})");
return 0.0;
}
return $this->calculateRecursive($userItem, $qualUserLevel);
}
/**
* Führt die eigentliche Berechnung basierend auf der Differenz-Logik durch
*/
private function calculateRecursive(BusinessUserItemOptimized $userItem, $qualUserLevel): float
{
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
$totalGrowthBonus = 0.0;
// Iteriere über alle direkten Beine (Firstlines)
foreach ($userItem->businessUserItems as $childItem) {
// Hole die Volumen-Verteilung aus diesem Bein
// Array-Format: ['0.0' => 1000, '1.5' => 5000]
// Bedeutung: 1000 Punkte sind mit 0% geschützt, 5000 Punkte mit 1.5%
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
$alreadyDistributedPercent = (float) $protectedPercentStr;
// Differenz berechnen: Mein Anspruch MINUS was schon verteilt wurde
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
if ($mySharePercent > 0) {
$commission = round($volume / 100 * $mySharePercent, 2);
$totalGrowthBonus += $commission;
}
}
}
return $totalGrowthBonus;
}
/**
* Liefert detaillierte Informationen zur Berechnung für die Anzeige
*
* @return array Detaillierte Aufschlüsselung pro Bein
*/
public function getCalculationDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
{
$details = [];
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return $details;
}
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
// Iteriere über alle direkten Beine (Firstlines)
foreach ($userItem->businessUserItems as $childItem) {
$legDetails = [
'user_id' => $childItem->user_id,
'first_name' => $childItem->first_name,
'last_name' => $childItem->last_name,
'level_name' => $childItem->user_level_name,
'volume_distribution' => [],
'total_commission' => 0.0,
'total_volume' => 0.0
];
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
$alreadyDistributedPercent = (float) $protectedPercentStr;
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
$commission = 0.0;
if ($mySharePercent > 0) {
$commission = round($volume / 100 * $mySharePercent, 2);
}
$legDetails['volume_distribution'][] = [
'protected_percent' => $alreadyDistributedPercent,
'volume' => $volume,
'my_share_percent' => $mySharePercent,
'commission' => $commission
];
$legDetails['total_commission'] += $commission;
$legDetails['total_volume'] += $volume;
}
// Sortiere nach Protection Level
usort($legDetails['volume_distribution'], function ($a, $b) {
return $a['protected_percent'] <=> $b['protected_percent'];
});
if ($legDetails['total_volume'] > 0) {
$details[] = $legDetails;
}
}
// Sortiere Beine nach höchster Provision
usort($details, function ($a, $b) {
return $b['total_commission'] <=> $a['total_commission'];
});
return $details;
}
/**
* Liefert eine Matrix-Sicht für die detaillierte Darstellung
* Zeilen = Beine (Legs), Spalten = Ebenen (Levels)
*/
public function getMatrixDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
{
$details = [];
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return $details;
}
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
foreach ($userItem->businessUserItems as $childItem) {
$legData = [
'user' => [
'id' => $childItem->user_id,
'name' => $childItem->first_name . ' ' . $childItem->last_name,
'level' => $childItem->user_level_name
],
'levels' => [],
'total_commission' => 0.0,
'total_volume' => 0.0
];
// Rekursiv die Ebenen dieses Beins einsammeln
// Start bei Ebene 1 (das ist das Kind selbst)
// Initial Protection ist 0 (vom Upline/Mir kommt kein Schutz, der relevant wäre, da ICH ja der Empfänger bin)
$this->collectLegLevels($childItem, 1, 0.0, $myGrowthPercent, $legData);
if (!empty($legData['levels'])) {
// Sortieren nach Ebenen-Index
ksort($legData['levels']);
$details[] = $legData;
}
}
// Sortieren nach Gesamt-Provision
usort($details, function ($a, $b) {
return $b['total_commission'] <=> $a['total_commission'];
});
return $details;
}
/**
* Rekursive Hilfsfunktion für Matrix-Daten
*/
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
{
// 1. Eigenen Status ermitteln (Schutz für Downline)
// WICHTIG: getQualifiedGrowthBonus() funktioniert sowohl für:
// - Live-berechnete Daten (qualificationCalculated = true)
// - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden)
$userProtection = $item->getQualifiedGrowthBonus();
$userLevelName = '';
if ($userProtection > 0) {
$qual = $item->getQualUserLevel();
$userLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
}
// Berechnung für diesen User (Ebene)
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
// Auch User ohne Volumen in die Matrix aufnehmen, wenn sie einen Status haben (Blocker sichtbar machen)
// Aber wir brauchen Volumen für die Relevanz. Wenn Volumen 0, dann ist der Block hier (noch) egal,
// wirkt aber auf die Ebenen darunter.
if ($volume > 0 || $userProtection > 0) {
// Differenz: Mein Anspruch - Schutz von oben
$diffPercent = max(0, $myPercent - $incomingProtection);
$commission = round($volume / 100 * $diffPercent, 2);
if (!isset($legData['levels'][$level])) {
$legData['levels'][$level] = [
'volume' => 0.0,
'commission' => 0.0,
'details' => [],
'has_blocker' => false, // Flag für UI
'blocker_name' => ''
];
}
$legData['levels'][$level]['volume'] += $volume;
$legData['levels'][$level]['commission'] += $commission;
// Markiere Blocker
if ($userProtection > 0) {
$legData['levels'][$level]['has_blocker'] = true;
$legData['levels'][$level]['blocker_name'] = $userLevelName . ' (' . $userProtection . '%)';
}
// Detail-Information für Hover/Debug
$legData['levels'][$level]['details'][] = [
'u' => $item->user_id,
'n' => $item->first_name . ' ' . $item->last_name, // Name für Tooltip
'v' => $volume,
'p_in' => $incomingProtection,
'p_own' => $userProtection,
'pct' => $diffPercent
];
$legData['total_volume'] += $volume;
$legData['total_commission'] += $commission;
}
// Protection für nächste Ebene: Maximum aus was von oben kam und was dieser User beansprucht
$nextProtection = max($incomingProtection, $userProtection);
// Rekursion (Begrenzt auf 30 Ebenen für Anzeige)
if ($level < 30 && !empty($item->businessUserItems)) {
foreach ($item->businessUserItems as $child) {
$this->collectLegLevels($child, $level + 1, $nextProtection, $myPercent, $legData);
}
}
}
/**
* Liefert das Volumen der Downline eines Users gruppiert nach dem "bereits verteilten Prozentsatz" (Protection Level).
* Rekursive Funktion, die die "Differenz-Logik" vorbereitet.
*
* @param BusinessUserItemOptimized $item
* @return array<string, float> Key = Protected Percent, Value = Volume Points
*/
public function getVolumeByProtectionLevel(BusinessUserItemOptimized $item, int $depth = 0): array
{
// Schutz vor zu tiefer Rekursion (Performance)
$maxDepth = 20;
if ($depth > $maxDepth) {
Log::warning("GrowthBonusCalculator: Max recursion depth reached for user {$item->user_id}");
return [];
}
// Bei Live-Berechnung: Qualifikation berechnen falls nötig
// Bei gespeicherten Daten: qual_user_level ist bereits vorhanden
if (!$item->isQualificationCalculated() && !$item->isQualLevel()) {
$item->calcQualPP();
}
$volumes = [];
// 1. Eigenes Volumen (Unprotected / GAP)
// Man selbst schützt seinen eigenen Umsatz NICHT vor der Upline.
// Daher Start mit Protection Level 0.0
$ownVolume = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($ownVolume > 0) {
$key = '0.0';
$volumes[$key] = $ownVolume;
}
// 2. Mein Schutz-Level ermitteln (für das Volumen, das durch mich hindurch fließt)
// WICHTIG: getQualifiedGrowthBonus() funktioniert sowohl für:
// - Live-berechnete Daten (qualificationCalculated = true)
// - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden)
$myProtectionPercent = $item->getQualifiedGrowthBonus();
// 3. Kinder laden falls nicht vorhanden (für gespeicherte Daten)
if (empty($item->businessUserItems) && $item->user_id) {
$this->loadChildrenFromDatabase($item, $depth);
}
// 4. Rekursive Aggregation der Kinder
if (!empty($item->businessUserItems)) {
foreach ($item->businessUserItems as $childItem) {
// Rekursiver Aufruf
$childVolumes = $this->getVolumeByProtectionLevel($childItem, $depth + 1);
// Schutz-Level anwenden und aggregieren
foreach ($childVolumes as $protectedPercentStr => $vol) {
$incomingProtection = (float) $protectedPercentStr;
// Das Volumen ist bereits mit $incomingProtection geschützt.
// Da es nun durch diesen User fließt, erhöht sich der Schutz auf dessen Level (falls höher).
$effectiveProtection = max($incomingProtection, $myProtectionPercent);
$newKey = (string) $effectiveProtection;
if (!isset($volumes[$newKey])) {
$volumes[$newKey] = 0.0;
}
$volumes[$newKey] += $vol;
}
}
}
return $volumes;
}
/**
* Lädt Kinder eines Users aus der Datenbank für die Growth Bonus Berechnung
*
* WICHTIG: Diese Methode wird nur aufgerufen, wenn businessUserItems leer ist
* (typischerweise bei gespeicherten Daten)
*/
private function loadChildrenFromDatabase(BusinessUserItemOptimized $item, int $depth): void
{
// Lade Sponsor-Beziehungen aus der User-Tabelle
$childIds = \App\User::where('m_sponsor', $item->user_id)
->where('deleted_at', null)
->pluck('id')
->toArray();
if (empty($childIds)) {
return;
}
// Hole das Date-Objekt vom Item
$date = $item->getDate();
if (!$date || !isset($date->month) || !isset($date->year)) {
Log::warning("GrowthBonusCalculator: No valid date for loading children of user {$item->user_id}");
return;
}
// Lade UserBusiness-Daten für alle Kinder
$childBusinesses = \App\Models\UserBusiness::whereIn('user_id', $childIds)
->where('month', $date->month)
->where('year', $date->year)
->get()
->keyBy('user_id');
foreach ($childIds as $childId) {
$childBusiness = $childBusinesses->get($childId);
// Nur Kinder mit Daten und Volumen berücksichtigen
if (!$childBusiness) {
continue;
}
$childTPSum = (float) ($childBusiness->sales_volume_points_TP_sum ?? 0);
// Nur relevante Kinder (mit Volumen oder qualifiziertem Level)
if ($childTPSum <= 0 && empty($childBusiness->qual_user_level)) {
continue;
}
// Erstelle ein BusinessUserItem aus den gespeicherten Daten
$childItem = new BusinessUserItemOptimized($date, null);
$childItem->makeUser($childId, false); // Aus DB laden
$childItem->addUserID();
$item->businessUserItems[] = $childItem;
}
}
}

View file

@ -0,0 +1,448 @@
# BusinessPlan System - Gesamtübersicht
## 📋 Inhaltsverzeichnis
1. [System-Architektur](#system-architektur)
2. [Datei-Übersicht](#datei-übersicht)
3. [Datenfluss](#datenfluss)
4. [Punktetypen & Begriffe](#punktetypen--begriffe)
5. [Level-System](#level-system)
6. [Provisionsberechnung](#provisionsberechnung)
7. [Cron-Job (Monatsabschluss)](#cron-job-monatsabschluss)
8. [Dashboard-Integration](#dashboard-integration)
---
## System-Architektur
```
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND / VIEWS │
├─────────────────────────────────────────────────────────────────────┤
│ dashboard/_statistics.blade.php │ dashboard/_points.blade.php │
│ user/team/*.blade.php │ admin/business/*.blade.php │
└───────────────────┬─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ CONTROLLER │
├─────────────────────────────────────────────────────────────────────┤
│ HomeController │ TeamController │ AdminController │
└───────────────────────┼─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ BUSINESS PLAN SERVICES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ TreeCalcBotOptimized.php │ │
│ │ - Hauptklasse für MLM-Strukturberechnungen │ │
│ │ - Initialisiert Business-User Strukturen │ │
│ │ - Berechnet Punkte über alle Ebenen │ │
│ │ - Delegiert an Repository, Renderer, Calculator │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ Business │ │ BusinessUserItem │ │ GrowthBonus │ │
│ │ User │ │ Optimized.php │ │ Calculator.php │ │
│ │ Repository │ │ │ │ │ │
│ │ .php │ │ - User-Datenbehälter │ │ - Tiefenbonus │ │
│ │ │ │ - Qualifikations- │ │ - Differenz- │ │
│ │ - DB Queries │ │ berechnung │ │ Logik │ │
│ │ - Caching │ │ - Provisions- │ │ │ │
│ │ - Relations │ │ berechnung │ │ │ │
│ └──────────────┘ └──────────────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SalesPointsVolume.php │ │
│ │ - Punkteerfassung bei Bestellungen │ │
│ │ - KP/TP Punkte-Unterscheidung │ │
│ │ - Neuberechnung bei Änderungen │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ MODELS │
├─────────────────────────────────────────────────────────────────────┤
│ User │ UserBusiness │ UserSalesVolume │
│ UserLevel │ UserBusinessStruct │ UserAbo │
│ ShoppingOrder │ ShoppingUser │ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Datei-Übersicht
| Datei | Zweck | Abhängigkeiten |
| ------------------------------- | ---------------------------------------- | ------------------------------ |
| `TreeCalcBotOptimized.php` | Hauptklasse für MLM-Strukturberechnungen | Repository, Renderer, Logger |
| `BusinessUserItemOptimized.php` | User-Datenbehälter mit Berechnungslogik | GrowthBonusCalculator |
| `BusinessUserRepository.php` | Optimierte DB-Abfragen mit Caching | User, UserBusiness Models |
| `GrowthBonusCalculator.php` | Tiefenbonus-Berechnung (Differenz-Logik) | BusinessUserItemOptimized |
| `SalesPointsVolume.php` | Punkteerfassung bei Bestellungen | UserSalesVolume, ShoppingOrder |
| `TreeHtmlRenderer.php` | HTML-Ausgabe für Struktur-Ansichten | - |
| `TreeHelperOptimized.php` | Hilfsfunktionen für Tree-Operationen | - |
### Dokumentation
| Datei | Inhalt |
| ------------------------- | ---------------------------------------- |
| `TreeCalcBotOptimized.md` | Technische Dokumentation der Hauptklasse |
| `Growth-Bonus.md` | Erklärung der Tiefenbonus-Logik |
| `Growth-Bonus-Matrix.md` | Matrix-Darstellung des Tiefenbonus |
---
## Datenfluss
### 1. Punkteerfassung (bei Bestellung)
```
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ ShoppingOrder │────▶│ SalesPointsVolume │────▶│ UserSalesVolume │
│ wird erstellt │ │ ::addSalesPoints │ │ wird erstellt │
└──────────────────┘ │ VolumeUser() │ └──────────────────┘
└────────────────────┘ │
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────┐
│ reCalculateSales │────▶│ month_KP_points │
│ PointsVolume() │ │ month_TP_points │
└────────────────────┘ │ month_shop_points│
└──────────────────┘
```
### 2. Strukturberechnung (Live oder Cron)
```
┌───────────────────────────────────────────────────────────────────────┐
│ TreeCalcBotOptimized │
│ │
│ 1. initStructureAdmin() oder initStructureUser() │
│ │ │
│ ├─▶ Prüfe gespeicherte Struktur (UserBusinessStructure) │
│ │ └─▶ Falls vorhanden und !forceLiveCalculation → laden │
│ │ │
│ └─▶ buildFreshStructure() │
│ │ │
│ ├─▶ loadRootUsers() - Top-Sponsoren ohne Parent │
│ │ │
│ ├─▶ loadParentsUsers() - Rekursive Downline │
│ │ └─▶ readParentsBusinessUsers() für jeden User │
│ │ │
│ ├─▶ calculateUserPointsOptimized() │
│ │ └─▶ Punkte pro Ebene (business_lines[1..n]) │
│ │ │
│ └─▶ calcQualPP() für jeden User │
│ ├─▶ calcuQualLevel() - Erreichte Stufe │
│ ├─▶ setNextUserLevel() - Nächste Stufe │
│ └─▶ calculateCommissions() - Provisionen │
│ ├─▶ Payline-Provisionen (Ebene 1-6) │
│ └─▶ Growth Bonus (ab Ebene 7+) │
│ └─▶ GrowthBonusCalculator │
└───────────────────────────────────────────────────────────────────────┘
```
### 3. Monatlicher Cron-Job
```
┌───────────────────────────────────────────────────────────────────────┐
│ BusinessUsersStoreOptimized (Cron) │
│ │
│ Ausführung: Einmal pro Monat (nach Monatsende) │
│ │
│ 1. storeUserBusinessStructure() │
│ └─▶ Erstellt UserBusinessStructure mit allen User-IDs │
│ │
│ 2. storeBusinessUsersDetail() │
│ └─▶ Für jeden User: initBusinesslUserDetail() mit forceLive=true │
│ └─▶ Speichert UserBusiness Datensatz │
│ │
│ 3. storeBusinessCompleted() │
│ └─▶ Markiert Struktur als "completed" │
└───────────────────────────────────────────────────────────────────────┘
```
---
## Punktetypen & Begriffe
### Punktearten
| Kürzel | Name | Beschreibung |
| ------ | -------------- | --------------------------------------------------- |
| **KP** | Kunden-Punkte | Eigene Bestellungen + Kundenbestellungen (Shop) |
| **TP** | Team-Punkte | KP + Punkte aus der Downline (Payline) |
| **PP** | Payline-Punkte | Summe der TP aus allen Ebenen bis zur Payline-Tiefe |
### Felder in UserSalesVolume
| Feld | Beschreibung |
| ------------------- | --------------------------------------------------------------------------- |
| `points` | Punkte dieser einzelnen Transaktion |
| `month_KP_points` | Kumulierte KP-Punkte des Monats |
| `month_TP_points` | Kumulierte TP-Punkte des Monats |
| `month_shop_points` | Kumulierte Shop-Punkte des Monats |
| `status` | 1=Berater-Bestellung, 2=Shop, 3=Shop-Pending, 4=Gutschrift, 5=Registrierung |
| `status_points` | 1=KP+TP, 2=nur KP |
### Felder in UserBusiness
| Feld | Beschreibung |
| ---------------------------- | ---------------------------------------------------------- |
| `sales_volume_KP_points` | KP-Punkte |
| `sales_volume_TP_points` | TP-Punkte |
| `sales_volume_points_shop` | Shop-Punkte |
| `sales_volume_points_KP_sum` | KP + Shop |
| `sales_volume_points_TP_sum` | TP + Shop |
| `payline_points` | Summe der Ebenen 1 bis Payline-Tiefe |
| `payline_points_qual_kp` | payline_points + Rest-KP |
| `business_lines` | JSON: Punkte pro Ebene {1: {points: X}, 2: {points: Y}...} |
| `qual_user_level` | Erreichtes Qualifikations-Level (Array) |
| `qual_user_level_next` | Nächste Provisions-Stufe |
| `next_qual_user_level` | Nächstes erreichbares Level |
| `commission_pp_total` | Payline-Provision |
| `commission_shop_sales` | Shop-Provision |
| `commission_growth_total` | Growth-Bonus (Tiefenbonus) |
---
## Level-System
### Qualifikationsbedingungen
| Level | Name | Min. KP (qual_kp) | Min. PP (qual_pp) | Paylines | Growth Bonus |
| ----- | -------------------- | ----------------- | ----------------- | -------- | ------------ |
| 1 | Junior Berater | 150 | 0 | 3 | - |
| 2 | Aktiv Junior Berater | 250 | 500 | 3 | - |
| 3 | Berater | 350 | 1.000 | 4 | - |
| 4 | Aktiv Berater | 450 | 2.500 | 5 | - |
| 5 | Vertriebspartner | 600 | 5.000 | 6 | - |
| 6 | Vertriebsleiter | 600 | 9.000 | 6 | - |
| 7 | Bronze Member | 600 (690\*) | 18.000 | 6 | 1,0% |
| 8 | Silber Member | 600 | 30.000 | 6 | 1,5% |
| 9 | Gold Member | 600 | 50.000 | 6 | 2,0% |
| 10 | Diamant Member | 600 | 100.000 | 6 | 2,5% |
| 11 | Platin Member\* | 600 | 250.000 | 7 | 3,0% |
| 12 | Platin Member\*\* | 600 | 500.000 | 7 | 3,5% |
| 13 | Platin Member\*\*\* | 600 | 1.000.000 | 8 | 4,0% |
\*690 Punkte zur Auszahlung (Schecksicherung)
### Payline-Prozentsätze (pr_line_X) - OHNE Growth Bonus
**⚠️ Diese Werte sind NUR der Payline-Anteil. Growth Bonus wird separat berechnet!**
| Level | E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | Growth |
| -------------------- | --- | --- | --- | --- | --- | --- | --- | --- | ------ |
| Junior Berater | 6% | 3% | 1% | - | - | - | - | - | - |
| Aktiv Junior Berater | 6% | 4% | 2% | - | - | - | - | - | - |
| Berater | 6% | 5% | 3% | 2% | - | - | - | - | - |
| Aktiv Berater | 6% | 5% | 4% | 2% | 1% | - | - | - | - |
| Vertriebspartner | 6% | 6% | 5% | 3% | 2% | 1% | - | - | - |
| Vertriebsleiter | 6% | 6% | 6% | 4% | 2% | 1% | - | - | - |
| Bronze Member | 6% | 6% | 6% | 4% | 2% | 2% | - | - | 1,0% |
| Silber Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 1,5% |
| Gold Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 2,0% |
| Diamant Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 2,5% |
| Platin Member\* | 7% | 7% | 7% | 4% | 3% | 2% | 1% | - | 3,0% |
| Platin Member\*\* | 7% | 7% | 7% | 4% | 3% | 2% | 1% | - | 3,5% |
| Platin Member\*\*\* | 7% | 7% | 7% | 4% | 4% | 3% | 2% | 1% | 4,0% |
**Gesamt-Provision pro Ebene = pr_line_X + Growth Bonus (mit Differenz-Logik)**
### Qualifikations-Algorithmus
```php
// In BusinessUserItemOptimized::calcuQualLevel()
1. Hole alle Levels wo qual_kp <= User's KP-Summe
2. Sortiere nach Position (höchstes zuerst)
3. Für jedes Level:
a. Berechne payline_points für dieses Level (Ebenen 1 bis paylines)
b. Berechne rest_kp = max(0, KP_sum - Level.qual_kp)
c. payline_points_qual_kp = payline_points + rest_kp
d. Wenn payline_points_qual_kp >= Level.qual_pp → QUALIFIZIERT
4. Return erstes qualifiziertes Level (höchstes)
```
---
## Provisionsberechnung
### 1. Payline-Provisionen (Unilevel)
Feste Prozentsätze auf Teamumsatz, begrenzt auf Payline-Tiefe.
**⚠️ WICHTIG:** Die `pr_line_X` Werte in der DB sind **NUR Payline** (ohne Growth Bonus)!
**Beispiel Gold Member (6 Paylines) - korrekte Werte:**
| Ebene | Payline (`pr_line_X`) | Growth Bonus | Gesamt |
| ----- | --------------------- | ------------ | ------ |
| 1 | 7% | +2% | 9% |
| 2 | 7% | +2% | 9% |
| 3 | 7% | +2% | 9% |
| 4 | 4% | +2% | 6% |
| 5 | 2% | +2% | 4% |
| 6 | 2% | +2% | 4% |
| 7+ | - | +2% | 2% |
```php
// In BusinessUserItemOptimized::calculateCommissions()
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
$points = $business_lines[$i]['points'];
$margin = $qual_user_level['pr_line_' . $i]; // NUR Payline-Prozentsatz
$commission = round($points / 100 * $margin, 2);
}
// Growth Bonus wird SEPARAT berechnet (siehe unten)
```
### 2. Growth Bonus (Tiefenbonus / Differenz-Bonus)
**Aktivierung:** Ab Bronze Member (Level 7+)
**⚠️ Bug-Fix November 2025:**
- **VOR Nov 2025:** Growth Bonus war IN den `pr_line_X` Werten enthalten UND wurde nochmal separat berechnet = **Doppelte Auszahlung!**
- **AB Nov 2025:** Growth Bonus wird NUR separat berechnet, `pr_line_X` enthält nur Payline-Anteil
**Logik:** Differenz zwischen eigenem Anspruch und bereits verteiltem Prozentsatz
```
Beispiel:
- User A: Diamant (2,5% Anspruch)
- User B (in A's Downline): Silber (1,5% Anspruch)
Punkte UNTER B:
- B erhält: 1,5% (sein voller Anspruch)
- A erhält: 2,5% - 1,5% = 1,0% (Differenz)
Punkte von B selbst (GAP):
- B erhält: 0% (kein Bonus auf sich selbst)
- A erhält: 2,5% (voller Anspruch, da B nicht "schützt")
```
**Implementation:** Siehe `GrowthBonusCalculator.php` und `Growth-Bonus.md`
### 3. Shop-Provision
```php
$commission_shop_sales = sales_volume_total_shop / 100 * margin_shop;
```
---
## Cron-Job (Monatsabschluss)
### Datei: `app/Cron/BusinessUsersStoreOptimized.php`
### Ausführung
```bash
# Typischerweise am 1. des Folgemonats
php artisan schedule:run
# Oder manuell:
php artisan business:store-monthly {month} {year}
```
### Ablauf
1. **storeUserBusinessStructure()**
- Prüft ob bereits Struktur für Monat/Jahr existiert
- Erstellt neue `UserBusinessStructure` mit allen User-IDs
- Speichert komplette Baumstruktur als JSON
2. **storeBusinessUsersDetail()**
- Iteriert über alle User (aus users Array)
- Für jeden nicht-abgeschlossenen User:
- `TreeCalcBotOptimized::initBusinesslUserDetail(user, forceLive=true)`
- Speichert `UserBusiness` Datensatz
- Markiert User als "completed" in Struktur
3. **storeBusinessCompleted()**
- Prüft ob alle User abgeschlossen
- Setzt `completed = true` auf Struktur
### Performance
- Typische Laufzeit: 10-60 Minuten (je nach User-Anzahl)
- Memory: 512MB-1GB empfohlen
- Kann in Batches/Chunks aufgeteilt werden
---
## Dashboard-Integration
### Dashboard-Statistiken (`dashboard/_statistics.blade.php`)
| Metrik | Datenquelle | Beschreibung |
| -------------------- | --------------------------------------------------------- | -------------------- |
| Kunden-Umsatz Punkte | `UserSalesVolume.getPointsKPSum()` | KP + Shop |
| Team-Umsatz Punkte | `UserBusiness.payline_points` | Payline-Summe |
| Direkte Neupartner | `User WHERE m_sponsor = $userId AND active_date IN month` | Neue Firstlines |
| Neupartner im Team | `UserSalesVolume WHERE status = 5 (registration)` | Registrierungspunkte |
| Kundenabos | `UserAbo WHERE member_id = $userId` | Kunden-Abos |
| Teamabos | `UserAbo WHERE user_id IN firstline_ids` | Team-Abos |
### Punkte-Tabelle (`dashboard/_points.blade.php`)
Zeigt detaillierte `UserSalesVolume` Einträge für gewählten Monat/Jahr:
- Datum, Punkte, Netto-Umsatz
- Status (Berater-Bestellung, Shop, Gutschrift, Registrierung)
- Bestellungs-Link, Kundeninformationen
---
## Erweiterungen & TODO
### Geplante Änderungen
1. **Dashboard-Statistiken erweitern** ✅
- Monats/Jahr-Filter implementiert
- Statistik-Kacheln hinzugefügt
2. **Marketingplan-Anpassungen** (in Arbeit)
- Level-Struktur überprüfen
- Provisionsberechnung validieren
- Growth-Bonus Differenz-Logik testen
### Bekannte Einschränkungen
- Growth Bonus nur ab November 2025 mit neuer Differenz-Logik
- Vor November 2025: Legacy-Berechnung (pauschal ab Ebene 7+)
- Struktur-Tiefe begrenzt auf 20-30 Ebenen (Performance)
---
## Kontakt & Wartung
**Letzte Aktualisierung:** Dezember 2025
**Version:** BusinessPlan System v2.0
### Log-Dateien
- `storage/logs/laravel.log` - Allgemeine Logs
- BusinessUserItem/TreeCalcBot Logs mit Prefix "BusinessUserItem:" / "TreeCalcBot:"
### Cache löschen
```php
// In Repository
$repository->clearCache();
// Oder manuell
Cache::forget("stored_structure_{$month}_{$year}");
Cache::forget("root_users_{$month}_{$year}");
```

View file

@ -1,25 +1,28 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use App\Services\Util;
use App\Models\ShoppingOrder;
use App\Models\UserSalesVolume;
use App\Services\Util;
use App\User;
use stdClass;
class SalesPointsVolume
{
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id){
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id)
{
if ($shoppingOrder->user_sales_volume) {
$to_user_id = intval($to_user_id);
if ($shoppingOrder->user_sales_volume->user_id === $to_user_id) {
\Session()->flash('alert-error', 'Keine Änderung: selber Berater');
return;
}
if (! $shoppingOrder->user_sales_volume->isCurrentMonthYear()) {
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
@ -45,12 +48,11 @@ class SalesPointsVolume
self::reCalculateSalesPointsVolume($to_user_id, $month, $year);
self::reCalculateSalesPointsVolume($form_user_id, $month, $year);
\Session()->flash('alert-save', true);
}
}
}
private static function add_KP_TP_Points($userSalesVolume, $month_points){
private static function add_KP_TP_Points($userSalesVolume, $month_points)
{
if ($userSalesVolume->status_points === 2) { // KP
$month_points->KP += $userSalesVolume->points;
} else {
@ -58,13 +60,15 @@ class SalesPointsVolume
$month_points->KP += $userSalesVolume->points;
$month_points->TP += $userSalesVolume->points;
}
return $month_points;
}
public static function reCalculateSalesPointsVolume($user_id, $month, $year){
public static function reCalculateSalesPointsVolume($user_id, $month, $year)
{
$userSalesVolumes = UserSalesVolume::where('user_id', $user_id)->where('month', $month)->where('year', $year)->orderBy('id', 'ASC')->get();
$month_points = new stdClass();
$month_points = new stdClass;
$month_points->KP = 0;
$month_points->TP = 0;
$month_total_net = 0;
@ -107,8 +111,8 @@ class SalesPointsVolume
}
}
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder){
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder)
{
/*
status
@ -124,9 +128,8 @@ class SalesPointsVolume
$year = date('Y');
$date = date('d.m.Y');
if ($status === 3) { // shop bestellung User pending if is_like
$user_id = NULL;
$user_id = null;
}
$user_sales_volume = UserSalesVolume::create([
'user_id' => $user_id,
@ -146,10 +149,10 @@ class SalesPointsVolume
}
return $user_sales_volume;
}
public static function setToUserAndReCalculate(UserSalesVolume $user_sales_volume, $user_id){
public static function setToUserAndReCalculate(UserSalesVolume $user_sales_volume, $user_id)
{
// set month year date new, calculate it in the currently month!
// If the month has changed, it can no longer be added to the month before
@ -167,30 +170,35 @@ class SalesPointsVolume
self::reCalculateSalesPointsVolume($user_id, $month, $year);
}
public static function getStatusByOrderPaymentFor(ShoppingOrder $shoppingOrder){
public static function getStatusByOrderPaymentFor(ShoppingOrder $shoppingOrder)
{
if ($shoppingOrder->payment_for) {
if ($shoppingOrder->payment_for === 6) { // Kunde-Shop
if ($shoppingOrder->shopping_user && $shoppingOrder->shopping_user->is_like) {
return 3; // shop Kunden, berater zuordnen <- need?
}
return 2;
}
return 1;
}
return 0;
}
public static function editSalesPointsVolume($data){
public static function editSalesPointsVolume($data)
{
$user_sales_volume = UserSalesVolume::findOrFail($data['id']);
if (! $user_sales_volume->isCurrentMonthYear()) {
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
$old_points = $user_sales_volume->points;
$old_total_net = $user_sales_volume->total_net;
$user_sales_volume->total_net = Util::reFormatNumber($data['total_net']);
$user_sales_volume->points = intval($data['points']);
$user_sales_volume->points = Util::reFormatNumber($data['points']);
$user_sales_volume->message = 'geändert: ' . date('d.m.Y');
$user_sales_volume->info = $data['info'];
@ -205,16 +213,15 @@ class SalesPointsVolume
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points geändert");
return;
\Session()->flash('alert-success', 'Points geändert');
}
public static function addSalesPointsVolume($data){
public static function addSalesPointsVolume($data)
{
if (! isset($data['user_id'])) {
\Session()->flash('alert-error', 'Kein Berater ausgewählt');
return;
}
$user = User::findOrFail($data['user_id']);
@ -223,7 +230,7 @@ class SalesPointsVolume
$date = date('d.m.Y');
$total_net = isset($data['total_net']) ? Util::reFormatNumber($data['total_net']) : 0;
$points = isset($data['points']) ? intval($data['points']) : 0;
$points = isset($data['points']) ? Util::reFormatNumber($data['points']) : 0;
$syslog[date('d.m.Y-h:i:s')] = 'add points: #' . $points . ' total: #' . $total_net;
$status = isset($data['status']) ? intval($data['status']) : 4;
$status_turnover = isset($data['status_turnover']) ? intval($data['status_turnover']) : null;
@ -244,14 +251,8 @@ class SalesPointsVolume
'status' => $status,
]);
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points hinzugefügt");
\Session()->flash('alert-success', 'Points hinzugefügt');
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
@ -18,7 +19,8 @@ class TreeCalcBot
private $init_from;
private static $userIDs = [];
public static function addUserID($id){
public static function addUserID($id)
{
self::$userIDs[$id] = $id;
}
@ -31,7 +33,6 @@ class TreeCalcBot
$this->date->start_date = $date->format('Y-m-d H:i:s');
$this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s');
$this->init_from = $init_from;
}
public function initStructureAdmin($check = true, $forceLiveCalculation = false)
@ -91,7 +92,8 @@ class TreeCalcBot
$this->business_user->storeUser();
}*/
public static function isFromStored($month, $year){
public static function isFromStored($month, $year)
{
//when is stored an completed
$UserBusinessStructure = UserBusinessStructure::where('year', $year)->where('month', $month)->first();
if ($UserBusinessStructure && $UserBusinessStructure->completed) {
@ -100,7 +102,8 @@ class TreeCalcBot
return false;
}
private function calcUserPoints($businessUserItems, $line){
private function calcUserPoints($businessUserItems, $line)
{
if (!isset($this->business_user->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
@ -117,7 +120,8 @@ class TreeCalcBot
}
}
public function getGrowthBonus(){
public function getGrowthBonus()
{
if (count($this->business_user->business_lines) > 6) {
$b_lines = $this->business_user->business_lines->toArray();
return array_slice($b_lines, 6);
@ -126,7 +130,8 @@ class TreeCalcBot
}
public function getKeybyLine($line, $key){
public function getKeybyLine($line, $key)
{
if ($this->business_user->business_lines) {
$b_lines = $this->business_user->business_lines;
if (isset($b_lines[$line])) {
@ -139,14 +144,14 @@ class TreeCalcBot
return $b_lines[$line][$key];
}
}
}
}
return 0;
}
//* reading from current*//
private function readRootUsers(){
private function readRootUsers()
{
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
@ -166,13 +171,15 @@ class TreeCalcBot
}
}
private function readParentsUsers(){
private function readParentsUsers()
{
foreach ($this->business_users as $business_user) {
$business_user->readParentsBusinessUsers();
}
}
private function readParentlessUser(){
private function readParentlessUser()
{
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
@ -192,7 +199,8 @@ class TreeCalcBot
//* reading from stored*//
private function readStoredRootUsers(UserBusinessStructure $userBusinessStructure){
private function readStoredRootUsers(UserBusinessStructure $userBusinessStructure)
{
//first level is root
if ($userBusinessStructure->structure) {
foreach ($userBusinessStructure->structure as $obj) {
@ -200,32 +208,33 @@ class TreeCalcBot
$BusinessUserItem->makeUser($obj->user_id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
}
}
}
private function readStoredParentsUsers(UserBusinessStructure $userBusinessStructure){
private function readStoredParentsUsers(UserBusinessStructure $userBusinessStructure)
{
foreach ($this->business_users as $business_user) {
$business_user->readStoredParentsBusinessUsers($userBusinessStructure->structure);
}
}
private function readStoredParentlessUser(UserBusinessStructure $userBusinessStructure){
private function readStoredParentlessUser(UserBusinessStructure $userBusinessStructure)
{
if ($userBusinessStructure->parentless) {
foreach ($userBusinessStructure->parentless as $obj) {
if (!isset(self::$userIDs[$obj->user_id])) {
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$this->parentless[] = $BusinessUserItem;
}
}
}
}
public function readSponsorUser($user_id){
public function readSponsorUser($user_id)
{
$user = User::find($user_id);
$userSponsor = User::find($user->m_sponsor);
if ($userSponsor) {
@ -234,30 +243,32 @@ class TreeCalcBot
}
}
public function readStoredSponsorUser($user_id){
public function readStoredSponsorUser($user_id)
{
$this->sponsor = new BusinessUserItem($this->date);
$this->sponsor->makeUser($user_id);
}
public function getItems(){
public function getItems()
{
return $this->business_users;
}
public function makeHtmlTree(){
public function makeHtmlTree()
{
$deep = 0;
$ret = '<ol class="dd-list">';
foreach ($this->business_users as $business_user) {
$ret .= $this->addItem($business_user, $deep);
}
$ret .= '</ol>';
return $ret;
}
private function addItem($item, $deep){
private function addItem($item, $deep)
{
$button = '';
@ -287,7 +298,7 @@ class TreeCalcBot
<span class="badge badge-outline-default ' . ($item->active_account ? '' : 'text-muted') . '">' . \App\Services\TranslationHelper::transUserLevelName($item->user_level_name) . ' | ' . $item->m_account . '</span>
<br><span class="small">' .
($item->active_account ?
'<strong>'.__('team.total_points').': '.$item->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$item->sales_volume_KP_points.' | '.__('team.s').': '.$item->sales_volume_points_shop.' <strong>
'<strong>' . __('team.total_points') . ': ' . formatNumber($item->sales_volume_points_KP_sum) . '</strong> | ' . __('team.e') . ': ' . formatNumber($item->sales_volume_KP_points) . ' | ' . __('team.s') . ': ' . formatNumber($item->sales_volume_points_shop) . ' <strong>
| ' . __('team.net_turnover') . ': ' . formatNumber($item->sales_volume_total_sum) . ' &euro;</strong> | ' . __('team.e') . ': ' . formatNumber($item->sales_volume_total) . ' &euro; | ' . __('team.s') . ': ' . formatNumber($item->sales_volume_total_shop) . ' &euro;' .
$button
:
@ -301,10 +312,10 @@ class TreeCalcBot
</div>' .
$this->addParentItem($item, $deep) .
'</li>';
}
private function addParentItem($item, $deep){
private function addParentItem($item, $deep)
{
if ($item->businessUserItems) {
$ret = '<ol class="dd-list dd-nodrag">';
foreach ($item->businessUserItems as $parent) {
@ -317,11 +328,13 @@ class TreeCalcBot
}
public function isParentless(){
public function isParentless()
{
return $this->parentless ? true : false;
}
public function makeParentlessHtml(){
public function makeParentlessHtml()
{
$ret = "";
foreach ($this->parentless as $item) {
$ret .= '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
@ -355,7 +368,8 @@ class TreeCalcBot
return $ret;
}
public function makeSponsorHtml(){
public function makeSponsorHtml()
{
if ($this->sponsor) {
//' | <a href="' . route('admin_business_user_detail', [$this->sponsor->id]) . '" class="btn icon-btn btn-xs btn-secondary"><span class="fa fa-calculator"></span></a>'
@ -387,5 +401,4 @@ class TreeCalcBot
}
return __('team.no_sponsor_assigned');
}
}

File diff suppressed because it is too large Load diff

View file

@ -79,9 +79,11 @@ class TreeCalcBotOptimized
if ($storedStructure) {
$this->logger->info("Loading stored business structure for {$this->date->month}/{$this->date->year}");
$this->loadStoredStructure($storedStructure);
return;
} else {
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}");
$this->buildFreshStructure();
return;
}
} catch (\Exception $e) {
$this->logger->error("Error initializing admin structure: " . $e->getMessage());
@ -171,9 +173,8 @@ class TreeCalcBotOptimized
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date, $this);
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation); // ✅ Nutzt bereits User-Objekt
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation);
$this->businessUser->checkSponsor($user);
// Führe vollständige Berechnung durch, wenn:
@ -184,14 +185,22 @@ class TreeCalcBotOptimized
$this->logger->info("Forcing live calculation for user {$user->id}");
}
// Aufbau der Struktur für den User in die unendliche Tiefe
// Phase 1: Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers($forceLiveCalculation);
// Calculate Points in Lines (optimiert für Memory-Effizienz)
// Phase 2: Calculate Points in Lines (optimiert für Memory-Effizienz)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1, $this->businessUser);
}
// Qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
// Phase 3: Qualifikation für ALLE User in der Struktur berechnen (Bottom-Up)
// WICHTIG: Muss VOR der Root-Qualifikation erfolgen, damit die Kinder
// ihr qual_user_level haben (für Growth Bonus Differenz-Berechnung)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateQualificationsForStructure($this->businessUser->businessUserItems);
}
// Phase 4: Qualifikation für ROOT-User nach qual_kp und qual_pp
$this->businessUser->calcQualPP();
}
} catch (\Exception $e) {
@ -200,6 +209,96 @@ class TreeCalcBotOptimized
}
}
/**
* Berechnet Qualifikationen für alle User in der Struktur rekursiv (Bottom-Up)
*
* WICHTIG: Diese Methode muss NACH der Punkte-Aggregation aufgerufen werden!
* Sie stellt sicher, dass alle User in der Struktur ihr qual_user_level haben,
* was für die Growth Bonus Differenz-Berechnung benötigt wird.
*
* Der Ablauf ist:
* 1. Rekursiv zuerst die Kinder berechnen (Bottom-Up)
* 2. business_lines für diesen User berechnen (basierend auf seinen Kindern)
* 3. Qualifikation berechnen (verwendet business_lines für Payline-Punkte)
*
* @param array $businessUserItems Array von BusinessUserItemOptimized
*/
private function calculateQualificationsForStructure(array $businessUserItems): void
{
foreach ($businessUserItems as $item) {
// Rekursiv zuerst die Kinder berechnen (Bottom-Up)
// So haben tiefere Ebenen ihr qual_user_level bevor die höheren Ebenen berechnet werden
if (!empty($item->businessUserItems)) {
$this->calculateQualificationsForStructure($item->businessUserItems);
}
// Business Lines für diesen User berechnen (basierend auf seinen Kindern)
// WICHTIG: Dies ist nötig, damit getPointsforPayline() korrekt funktioniert
if (!empty($item->businessUserItems)) {
$this->calculateBusinessLinesForUser($item);
}
// Dann Qualifikation für diesen User berechnen
// Nur wenn noch nicht berechnet (Performance-Optimierung)
if (!$item->isQualificationCalculated()) {
$item->calcQualPP(false);
}
}
}
/**
* Berechnet die business_lines für einen einzelnen User basierend auf seinen Kindern
*
* Diese Methode aggregiert die Team-Punkte der Kinder in die business_lines,
* ähnlich wie calculateUserPointsOptimized, aber nur für einen einzelnen User.
*
* @param BusinessUserItemOptimized $user Der User, für den die business_lines berechnet werden
*/
private function calculateBusinessLinesForUser(BusinessUserItemOptimized $user): void
{
// Bereits berechnet? (business_lines existieren und haben Daten)
$existingLines = $user->business_lines;
if (!empty($existingLines) && count($existingLines) > 0) {
return;
}
// Initialisiere business_lines über die Methode
$user->initBusinessLines();
// Sammle alle Kinder rekursiv mit ihrer Tiefe
$this->collectChildrenPointsForUser($user->businessUserItems, 1, $user);
}
/**
* Rekursive Hilfsfunktion zum Sammeln der Punkte für business_lines
*
* @param array $children Die Kinder des Users
* @param int $line Die aktuelle Linie (Tiefe)
* @param BusinessUserItemOptimized $targetUser Der User, für den wir die business_lines bauen
*/
private function collectChildrenPointsForUser(array $children, int $line, BusinessUserItemOptimized $targetUser): void
{
foreach ($children as $child) {
// Initialisiere die Linie falls nötig
if (!$targetUser->hasBusinessLine($line)) {
$obj = new stdClass();
$obj->points = 0;
$targetUser->addBusinessLineToUser($line, $obj);
}
// Füge die Team-Punkte des Kindes hinzu
$points = (float) ($child->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$targetUser->addBusinessLinePoints($line, $points);
}
// Rekursiv für die Kinder des Kindes (nächste Linie)
if (!empty($child->businessUserItems)) {
$this->collectChildrenPointsForUser($child->businessUserItems, $line + 1, $targetUser);
}
}
}
/**
* Gibt Growth Bonus zurück (ab Linie 6)
* Erweitert um Array/Object-Kompatibilität für business_lines
@ -276,6 +375,15 @@ class TreeCalcBotOptimized
return $this->businessUsers;
}
/**
* Getter-Methoden (Rückwärtskompatibilität)
*/
public function getItem(): object
{
return $this->businessUser;
}
/**
* Zählt die Gesamtanzahl aller User in der Struktur (rekursiv)
*/

View file

@ -22,8 +22,8 @@ class TreeHelperOptimized
}
$qualKP = (int) $userBusiness->qual_kp;
$pointsSum = (int) $userBusiness->sales_volume_points_KP_sum;
$qualKP = (float) $userBusiness->qual_kp;
$pointsSum = (float) $userBusiness->sales_volume_points_KP_sum;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-info';
@ -39,8 +39,8 @@ class TreeHelperOptimized
return '-';
}
$qualKP = (int) $user->user_level->qual_kp;
$pointsSum = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$qualKP = (float) $user->user_level->qual_kp;
$pointsSum = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-warning-dark';
@ -54,9 +54,9 @@ class TreeHelperOptimized
public static function generateSalesVolumeDisplay(UserBusiness $userBusiness, string $type): string
{
if ($type === 'points') {
$total = (int) $userBusiness->sales_volume_points_KP_sum;
$individual = (int) $userBusiness->sales_volume_KP_points;
$shop = (int) $userBusiness->sales_volume_points_shop;
$total = (float) $userBusiness->sales_volume_points_KP_sum;
$individual = (float) $userBusiness->sales_volume_KP_points;
$shop = (float) $userBusiness->sales_volume_points_shop;
} else {
$total = (float) $userBusiness->sales_volume_total_sum;
$individual = (float) $userBusiness->sales_volume_total;
@ -64,9 +64,9 @@ class TreeHelperOptimized
$suffix = ' &euro;';
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$totalFormatted = formatNumber($total);
$individualFormatted = formatNumber($individual);
$shopFormatted = formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
@ -79,18 +79,18 @@ class TreeHelperOptimized
public static function generateSalesVolumeDisplayForUser(User $user, string $type, int $month, int $year): string
{
if ($type === 'points') {
$total = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$individual = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points');
$shop = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop');
$total = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$individual = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points');
$shop = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop');
} else {
$total = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_sum');
$individual = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total');
$shop = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_shop');
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$totalFormatted = formatNumber($total);
$individualFormatted = formatNumber($individual);
$shopFormatted = formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .

View file

@ -169,8 +169,8 @@ class TreeHtmlRenderer
// Qualifikation
$html .= '<div class="col-md-2">';
if (!empty($member->qual_kp)) {
$pointsSum = (int) ($member->sales_volume_points_KP_sum ?? 0);
$qualKP = (int) $member->qual_kp;
$pointsSum = (float) ($member->sales_volume_points_KP_sum ?? 0);
$qualKP = (float) $member->qual_kp;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-success' : 'badge-warning';
$html .= '<span class="badge ' . $badgeClass . '">KU ' . $qualKP . '</span>';
@ -341,9 +341,9 @@ class TreeHtmlRenderer
$eSum = $item->sales_volume_total ?: 0;
$sSum = $item->sales_volume_total_shop ?: 0;
return '<strong>' . __('team.total_points') . ': ' . $totalPoints . '</strong> | ' .
__('team.e') . ': ' . $ePoints . ' | ' .
__('team.s') . ': ' . $sPoints . ' <strong> | ' .
return '<strong>' . __('team.total_points') . ': ' . formatNumber($totalPoints) . '</strong> | ' .
__('team.e') . ': ' . formatNumber($ePoints) . ' | ' .
__('team.s') . ': ' . formatNumber($sPoints) . ' <strong> | ' .
__('team.net_turnover') . ': ' . formatNumber($totalSum) . ' &euro;</strong> | ' .
__('team.e') . ': ' . formatNumber($eSum) . ' &euro; | ' .
__('team.s') . ': ' . formatNumber($sSum) . ' &euro;';

View file

@ -2,8 +2,8 @@
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
use App\Models\ShoppingOrder;
/**
* DHL Data Helper
@ -19,11 +19,7 @@ class DhlDataHelper
* Structure matches official DHL API v2 createOrders endpoint:
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
*
* @param ShoppingOrder $order
* @param float $weight
* @param array $options
* @param array|null $dhlConfig Optional pre-loaded config (for queue jobs)
* @return array
*/
public static function prepareOrderData(ShoppingOrder $order, float $weight, array $options = [], ?array $dhlConfig = null): array
{
@ -36,10 +32,11 @@ class DhlDataHelper
// Get DHL configuration for shipper data
if ($dhlConfig === null) {
$settingController = new SettingController();
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
}
$dimensions = isset($dhlConfig['dimensions'][$options['product_code']]) ? $dhlConfig['dimensions'][$options['product_code']] : $dhlConfig['dimensions']['default'];
return [
'order_id' => $order->id,
'weight_kg' => $weight,
@ -72,6 +69,8 @@ class DhlDataHelper
'country' => $shippingAddress['country']?->code ?? 'DE',
'email' => $shippingAddress['email'] ?? '',
'phone' => $shippingAddress['phone'] ?? '',
// DHL Postnummer für Packstation/Paketbox
'postNumber' => $shippingAddress['postnumber'] ?? null,
// Store individual fields for easier access
'firstname' => $shippingAddress['firstname'] ?? '',
'lastname' => $shippingAddress['lastname'] ?? '',

View file

@ -2,10 +2,10 @@
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Models\Country;
use Illuminate\Support\Facades\Log;
use App\Models\ShoppingOrder;
use Exception;
use Illuminate\Support\Facades\Log;
/**
* DHL Modal Service
@ -34,6 +34,7 @@ class DhlModalService
* @param mixed $id Order ID or 'new'
* @param array $data Additional data from the request
* @return array Prepared data for the view
*
* @throws Exception
*/
public function prepareModalData($id, array $data): array
@ -47,7 +48,7 @@ class DhlModalService
'errors' => [],
'warnings' => [],
'existingShipments' => [],
'modalMode' => 'search' // 'search', 'create', 'info'
'modalMode' => 'search', // 'search', 'create', 'info'
];
// If no order ID or 'new', return empty data for order selection
@ -60,6 +61,7 @@ class DhlModalService
$order = $this->loadOrder($id);
if (! $order) {
$result['errors'][] = "Bestellung #{$id} wurde nicht gefunden.";
return $result;
}
@ -77,7 +79,7 @@ class DhlModalService
$result['modalMode'] = 'info';
Log::info('[DHL Modal] Order has existing shipments, showing info mode', [
'order_id' => $order->id,
'shipment_count' => count($existingShipments)
'shipment_count' => count($existingShipments),
]);
} else {
$result['modalMode'] = 'create';
@ -100,13 +102,13 @@ class DhlModalService
Log::info('[DHL Modal] Prepared modal data for creation', [
'order_id' => $order->id,
'weight' => $result['orderWeight'],
'address_valid' => empty($result['errors'])
'address_valid' => empty($result['errors']),
]);
}
} catch (Exception $e) {
Log::error('[DHL Modal] Error preparing modal data', [
'order_id' => $id,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
$result['errors'][] = 'Fehler beim Laden der Bestelldaten: ' . $e->getMessage();
@ -119,22 +121,18 @@ class DhlModalService
* Load order with required relationships
*
* @param mixed $id
* @return ShoppingOrder|null
*/
private function loadOrder($id): ?ShoppingOrder
{
return ShoppingOrder::with([
'shopping_order_items',
'shopping_user',
'dhlShipments' // Include DHL shipments
'dhlShipments', // Include DHL shipments
])->find($id);
}
/**
* Get existing DHL shipments for the order
*
* @param ShoppingOrder $order
* @return array
*/
private function getExistingShipments(ShoppingOrder $order): array
{
@ -160,16 +158,15 @@ class DhlModalService
'tracking_status_translated' => $shipment->tracking_status ? \Acme\Dhl\Models\DhlShipment::getStatusTranslationFor($shipment->tracking_status) : null,
'last_tracked_at' => $shipment->last_tracked_at,
'can_cancel' => $shipment->canCancel(),
'is_delivered' => $shipment->isDelivered()
'is_delivered' => $shipment->isDelivered(),
'email' => $shipment->email, // E-Mail für Tracking-E-Mail Button
'can_send_email' => $shipment->canSendTrackingEmail(),
];
})->toArray();
}
/**
* Calculate order weight in kg
*
* @param ShoppingOrder $order
* @return float
*/
private function calculateOrderWeight(ShoppingOrder $order): float
{
@ -209,9 +206,6 @@ class DhlModalService
/**
* Process and parse shipping address from order
*
* @param ShoppingOrder $order
* @return array
*/
private function processShippingAddress(ShoppingOrder $order): array
{
@ -237,6 +231,8 @@ class DhlModalService
'phone' => $useShipping ? ($shoppingUser->shipping_phone ?? '') : ($shoppingUser->billing_phone ?? ''),
'email' => $shoppingUser->billing_email ?? '',
'houseNumber' => '',
// DHL Postnummer für Packstation/Paketbox (nur bei Versandadresse)
'postnumber' => $useShipping ? ($shoppingUser->shipping_postnumber ?? '') : '',
];
// Parse and separate street name and number
@ -247,8 +243,6 @@ class DhlModalService
/**
* Parse street address and separate street name from house number
*
* @param array &$addressData
*/
private function parseStreetAddress(array &$addressData): void
{
@ -262,7 +256,7 @@ class DhlModalService
// Pattern 2: "Musterstraße 123-125" or "Musterstraße 123/125"
'/^(.+?)\s+(\d+[-\/]\d+[a-zA-Z]?)$/u',
// Pattern 3: "123 Musterstraße" (number first)
'/^(\d+[a-zA-Z]?)\s+(.+)$/u'
'/^(\d+[a-zA-Z]?)\s+(.+)$/u',
];
foreach ($patterns as $index => $pattern) {
@ -289,7 +283,6 @@ class DhlModalService
/**
* Validate address completeness and format
*
* @param array $address
* @return array Validation result with 'valid', 'errors', and 'warnings' keys
*/
private function validateAddress(array $address): array
@ -303,7 +296,7 @@ class DhlModalService
'lastname' => 'Nachname',
'address' => 'Straße',
'zipcode' => 'Postleitzahl',
'city' => 'Stadt'
'city' => 'Stadt',
];
foreach ($requiredFields as $field => $label) {
@ -337,14 +330,12 @@ class DhlModalService
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
'warnings' => $warnings,
];
}
/**
* Get empty address template
*
* @return array
*/
private function getEmptyAddress(): array
{
@ -359,6 +350,7 @@ class DhlModalService
'country' => null,
'phone' => '',
'email' => '',
'postnumber' => '',
];
}
@ -374,13 +366,11 @@ class DhlModalService
/**
* Get available DHL product codes from settings
*
* @return array
*/
private function getAvailableProductCodes(): array
{
// Get DHL configuration with merged settings
$settingController = new \App\Http\Controllers\SettingController();
$settingController = new \App\Http\Controllers\SettingController;
$dhlConfig = $settingController->getDhlConfig();
$productCodes = [];
@ -409,7 +399,7 @@ class DhlModalService
$productCodes = [
'V01PAK' => 'DHL Paket National',
'V53PAK' => 'DHL Paket International',
'V62WP' => 'DHL Warenpost National'
'V62WP' => 'DHL Warenpost National',
];
}
@ -419,7 +409,6 @@ class DhlModalService
/**
* Validate shipment parameters before API call
*
* @param array $shipmentData
* @return array Validation result
*/
public function validateShipmentData(array $shipmentData): array
@ -450,7 +439,7 @@ class DhlModalService
'shipping_houseNumber' => 'Hausnummer',
'shipping_zipcode' => 'Postleitzahl',
'shipping_city' => 'Stadt',
'shipping_country_id' => 'Land'
'shipping_country_id' => 'Land',
];
foreach ($requiredAddressFields as $field => $label) {
@ -462,15 +451,12 @@ class DhlModalService
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
'warnings' => $warnings,
];
}
/**
* Prepare address data for DHL API
*
* @param array $formData
* @return array
*/
public function prepareAddressForApi(array $formData): array
{
@ -491,7 +477,8 @@ class DhlModalService
'country_id' => $country?->id,
'country' => $country, // Store country object for DhlDataHelper
'phone' => trim($formData['shipping_phone'] ?? ''),
'email' => trim($formData['shipping_email'] ?? '') // Add email if available
'email' => trim($formData['shipping_email'] ?? ''), // Add email if available
'postnumber' => trim($formData['shipping_postnumber'] ?? ''), // DHL Postnummer für Packstation/Paketbox
];
}
}

View file

@ -2,9 +2,11 @@
namespace App\Services;
use Acme\Dhl\Models\DhlShipment;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
use App\Jobs\CreateShipmentJob;
use App\Jobs\CancelShipmentJob;
use App\Services\DhlDataHelper;
use Illuminate\Support\Facades\Log;
use Exception;
@ -144,4 +146,174 @@ class DhlShipmentService
];
}
}
/**
* Cancel a DHL shipment (sync or async based on config)
*
* @param DhlShipment $shipment
* @param array $options
* @return array
*/
public function cancelShipment(DhlShipment $shipment, array $options = []): array
{
// Get DHL configuration
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
// Check if queue should be used
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
return $this->cancelShipmentAsync($shipment, $options, $dhlConfig);
} else {
return $this->cancelShipmentSync($shipment, $options, $dhlConfig);
}
}
/**
* Cancel shipment asynchronously using queue
*
* @param DhlShipment $shipment
* @param array $options
* @param array $dhlConfig
* @return array
*/
private function cancelShipmentAsync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Dispatch job
CancelShipmentJob::dispatch($shipment, $options);
Log::info('[DHL Service] Shipment cancellation dispatched to queue', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no
]);
return [
'success' => true,
'message' => 'Sendung wird storniert...',
'queued' => true,
'shipment_id' => $shipment->id
];
} catch (Exception $e) {
Log::error('[DHL Service] Failed to dispatch shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return [
'success' => false,
'message' => 'Fehler beim Einreihen der Stornierung: ' . $e->getMessage(),
'queued' => false
];
}
}
/**
* Cancel shipment synchronously
*
* @param DhlShipment $shipment
* @param array $options
* @param array $dhlConfig
* @return array
*/
private function cancelShipmentSync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Validate shipment has DHL number
if (empty($shipment->dhl_shipment_no)) {
return [
'success' => false,
'message' => 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.',
'queued' => false,
'shipment_id' => $shipment->id
];
}
// Validate shipment can be cancelled
if (! $shipment->canCancel()) {
return [
'success' => false,
'message' => 'Sendung kann im aktuellen Status "' . $shipment->status . '" nicht storniert werden. Nur Status "created" oder "pending" sind stornierbar.',
'queued' => false,
'shipment_id' => $shipment->id
];
}
Log::info('[DHL Service] Cancelling shipment synchronously', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'base_url' => $dhlConfig['base_url']
]);
// Create DHL client
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
// Cancel the shipment directly
$success = $shippingService->cancelLabel($shipment->dhl_shipment_no);
if ($success) {
Log::info('[DHL Service] Shipment cancelled successfully (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no
]);
return [
'success' => true,
'message' => 'Sendung wurde erfolgreich storniert!',
'queued' => false,
'shipment_id' => $shipment->id
];
} else {
throw new Exception('Cancellation returned false');
}
} catch (\InvalidArgumentException $e) {
Log::warning('[DHL Service] Shipment cancellation validation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage()
]);
return [
'success' => false,
'message' => $e->getMessage(),
'queued' => false,
'shipment_id' => $shipment->id
];
} catch (Exception $e) {
Log::error('[DHL Service] Shipment cancellation failed (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'error' => $e->getMessage(),
'error_trace' => $e->getTraceAsString()
]);
// Check if it's an API authentication/resource error
$errorMessage = $e->getMessage();
if (strpos($errorMessage, 'RF-UndefinedResource') !== false) {
return [
'success' => false,
'message' => 'Die Sendung konnte bei DHL nicht gefunden werden. Mögliche Ursachen: Sendung wurde bereits storniert, ist zu alt, oder wurde in einem anderen Modus (Sandbox/Production) erstellt.',
'queued' => false,
'shipment_id' => $shipment->id,
'technical_error' => $errorMessage
];
}
return [
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: ' . $errorMessage,
'queued' => false,
'shipment_id' => $shipment->id
];
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\Attribute;
@ -13,8 +14,6 @@ use App\User;
class HTMLHelper
{
public static $months = [
1 => 'January',
2 => 'February',
@ -27,10 +26,9 @@ class HTMLHelper
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December'
12 => 'December',
];
private static $roles = [
0 => 'Berater',
1 => 'VIP',
@ -39,30 +37,35 @@ class HTMLHelper
4 => 'SySAdmin',
];
public static function getMonth($i){
public static function getMonth($i)
{
return trans('cal.months.'.self::$months[intval($i)]);
}
public static function getTransMonths(){
public static function getTransMonths()
{
$ret = [];
foreach (self::$months as $key => $val) {
$ret[$key] = trans('cal.months.'.$val);
}
return $ret;
}
public static function getYearRange($start = 2021)
{
$end = date("Y");
$end = date('Y');
return array_reverse(range($start, $end));
}
public static function getRoleLabel($role_id = 0){
public static function getRoleLabel($role_id = 0)
{
return '<span class="badge badge-pill '.self::getLabel($role_id).'">'.self::$roles[$role_id].'</span>';
}
public static function getLabel($id){
public static function getLabel($id)
{
switch ($id) {
case 0:
return 'badge-default';
@ -80,49 +83,60 @@ class HTMLHelper
return 'badge-primary';
break;
}
}
public static function getRolesOptions(){
$ret = "";
public static function getRolesOptions()
{
$ret = '';
foreach (self::$roles as $role_id => $value) {
$ret .= '<option value="'.$role_id.'">'.$value.'</option>\n';
}
return $ret;
}
public static function getYearSelectOptions(){
$start = date("Y", strtotime("-5 years", time()));
$end = date("Y", strtotime("+1 years", time()));
public static function getYearSelectOptions()
{
$start = date('Y', strtotime('-5 years', time()));
$end = date('Y', strtotime('+1 years', time()));
$values = range($start, $end);
$now = date("Y", time());
$ret = "";
$now = date('Y', time());
$ret = '';
foreach ($values as $value) {
$attr = ($value == $now) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getAboDeliveryOptions($default = 5){
public static function getAboDeliveryOptions($default = 5)
{
$values = \App\Models\UserAbo::$aboDeliveryDays;
$ret = "";
$ret = '';
foreach ($values as $value) {
$attr = ($value == $default) ? 'selected="selected"' : '';
$str = self::getAboStrLang($value);
$ret .= '<option value="'.$value.'" '.$attr.'>'.$str.'</option>\n';
}
return $ret;
}
public static function getAboStrLang($num){
public static function getAboStrLang($num)
{
return $num.'. '.__('abo.of_month');
}
public static function getAboFirstExecutionDate($date, $interval)
{
return AboHelper::getFirstAboDate($date, $interval)->format('d.m.Y');
}
public static function getAttributesWithoutParents($id = false, $sameId = false, $all = true){
public static function getAttributesWithoutParents($id = false, $sameId = false, $all = true)
{
$values = Attribute::where('parent_id', null)->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -133,12 +147,14 @@ class HTMLHelper
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getCategoriesWithoutParents($id = false, $sameId = false, $all = true){
public static function getCategoriesWithoutParents($id = false, $sameId = false, $all = true)
{
$values = Category::where('parent_id', null)->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -149,15 +165,17 @@ class HTMLHelper
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getProductsOptions($ids = array(), $all = true){
public static function getProductsOptions($ids = [], $all = true)
{
if ($ids == null) {
$ids = array();
$ids = [];
}
$values = Product::all();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -165,12 +183,14 @@ class HTMLHelper
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.' ('.($value->active ? 'on' : 'off').')</option>\n';
}
return $ret;
}
public static function getCategoriesOptions($ids = array(), $all = true){
public static function getCategoriesOptions($ids = [], $all = true)
{
$values = Category::where('active', 1)->orderBy('pos', 'DESC')->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -178,24 +198,54 @@ class HTMLHelper
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getProductIngredientsOptions($has_ids = array(), $all = true){
public static function getProductIngredientsOptions($has_ids = [], $all = true)
{
$values = Ingredient::where('active', 1)->get();
$ret = "";
$attr = "";
$ret = '';
$attr = '';
foreach ($values as $value) {
if (! in_array($value->id, $has_ids)) {
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
}
return $ret;
}
public static function getAttributesOptions($ids = array(), $all = true){
/**
* Erzeugt Options für Bundle-Produkt-Auswahl
* Filtert bereits enthaltene Produkte und das aktuelle Produkt selbst aus
*
* @param array $has_ids IDs der bereits enthaltenen Produkte
* @param int|null $exclude_product_id ID des aktuellen Produkts (um Selbst-Referenz zu vermeiden)
* @return string HTML Options
*/
public static function getProductBundleOptions($has_ids = [], $exclude_product_id = null)
{
$values = Product::where('active', 1)->orderBy('name')->get();
$ret = '';
foreach ($values as $value) {
// Überspringe bereits enthaltene Produkte und das Produkt selbst
if (! in_array($value->id, $has_ids) && $value->id != $exclude_product_id) {
$label = $value->name;
if ($value->number) {
$label = $value->number.' - '.$value->name;
}
$ret .= '<option value="'.$value->id.'">'.htmlspecialchars($label).'</option>\n';
}
}
return $ret;
}
public static function getAttributesOptions($ids = [], $all = true)
{
$values = Attribute::where('active', 1)->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -203,12 +253,14 @@ class HTMLHelper
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getUserLevelOptions($id = false, $all = true){
public static function getUserLevelOptions($id = false, $all = true)
{
$values = UserLevel::where('active', 1)->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
@ -216,27 +268,29 @@ class HTMLHelper
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getCompanyOptions($company){
$options = array(1 => __('business'), 0 => __('private'), );
$ret = "";
public static function getCompanyOptions($company)
{
$options = [1 => __('business'), 0 => __('private')];
$ret = '';
foreach ($options as $id => $value) {
$attr = ($id == $company) ? 'selected="selected"' : '';
$ret .= '<option value="'.$id.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getContriesWithMore($id, $all=true){#
public static function getContriesWithMore($id, $all = true)
{ //
$values = Country::all();
$counter = 1;
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value) {
if ($counter == 7) {
@ -248,22 +302,23 @@ class HTMLHelper
$counter++;
}
$ret .= '</optgroup>';
return $ret;
}
public static function getContriesCodes($id, $all=true){#
public static function getContriesCodes($id, $all = true)
{ //
$values = Country::all();
$counter = 1;
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value) {
if(!$value->phone) continue;
if (! $value->phone) {
continue;
}
if ($counter == 7) {
$ret .= '<optgroup label="'.__('further countrie').'">';
}
@ -273,13 +328,15 @@ class HTMLHelper
$counter++;
}
$ret .= '</optgroup>';
return $ret;
}
public static function getCountriesWithoutUsedShippings($all=true){#
public static function getCountriesWithoutUsedShippings($all = true)
{ //
$values = Country::all();
$country_ids = ShippingCountry::all()->pluck('country_id')->toArray();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
@ -288,100 +345,113 @@ class HTMLHelper
$ret .= '<option value="'.$value->id.'">'.$value->getLocated().'</option>\n';
}
}
return $ret;
}
public static function getCountryNameFormShipping($id){
public static function getCountryNameFormShipping($id)
{
$value = ShippingCountry::find($id);
if ($value) {
return $value->country->getLocated();
}
return "not defined";
return 'not defined';
}
public static function getCountriesForShipping($id, $all=false){#
public static function getCountriesForShipping($id, $all = false)
{ //
$values = ShippingCountry::all();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->country->getLocated().'</option>\n';
}
return $ret;
}
public static function getSalutation($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
$ret = "";
public static function getSalutation($id)
{
$values = ['mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV')];
$ret = '';
$ret .= '<option value="">'.__('please select').'</option>\n';
foreach ($values as $key => $value) {
$attr = ($key == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$key.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getSalutationLang($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
return (!empty($values[$id]) ? $values[$id] : '');
public static function getSalutationLang($id)
{
$values = ['mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV')];
return ! empty($values[$id]) ? $values[$id] : '';
}
public static function getTaxSaleOptions($id){
$values = array('1' => __('account.taxable_sales_1'), '2' => __('account.taxable_sales_2'));
$ret = "";
public static function getTaxSaleOptions($id)
{
$values = ['1' => __('account.taxable_sales_1'), '2' => __('account.taxable_sales_2')];
$ret = '';
$ret .= '<option value="">'.__('please select').'</option>\n';
foreach ($values as $key => $value) {
$attr = ($key == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$key.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getMembersOptions($id, $all=false){
public static function getMembersOptions($id, $all = false)
{
$values = User::where('active', '=', true)->where('blocked', '=', false)->where('payment_account', '>=', now())->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$to="";
$to = '';
if ($value->account) {
$to = $value->account->first_name." ".$value->account->last_name." | ";
$to = $value->account->first_name.' '.$value->account->last_name.' | ';
}
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$to.$value->email.' #'.$value->account->m_account.'</option>\n';
}
return $ret;
}
public static function getUserCustomerOptions($id, $all=false){
public static function getUserCustomerOptions($id, $all = false)
{
$values = ShoppingUser::select(['id', 'billing_firstname', 'billing_lastname', 'billing_email', 'number'])
->where('shopping_users.member_id', '=', \Auth::user()->id)->get();
$ret = "";
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$to = $value->billing_firstname." ".$value->billing_lastname." | ".$value->billing_email;
$to = $value->billing_firstname.' '.$value->billing_lastname.' | '.$value->billing_email;
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$to.' #'.$value->number.'</option>\n';
}
return $ret;
}
public static function getOptionRange($select, $from=1, $to=50){
public static function getOptionRange($select, $from = 1, $to = 50)
{
$values = range($from, $to);
$ret = "";
$ret = '';
foreach ($values as $value) {
$attr = ($value == $select) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
return $ret;
}
}

View file

@ -6,6 +6,7 @@ use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\User;
use Illuminate\Support\Collection;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
class LevelReportService
{
@ -67,16 +68,29 @@ class LevelReportService
// Lade aktuellen User Level
$currentUser = $currentUserLevels->get($userBusiness->user_id);
$currentUserLevelName = 'Unbekannt';
$currentUserLevel = null;
if ($currentUser && $currentUser->m_level) {
$currentUserLevel = $userLevels->get($currentUser->m_level);
$currentUserLevelName = $currentUserLevel ? $currentUserLevel->name : 'Level ID: ' . $currentUser->m_level;
}
// Lade neues Level für POS-Vergleich (wird für Filter und level_updated benötigt)
$newLevel = $userLevels->get($newLevelId);
$newLevelPos = $newLevelData['pos'] ?? ($newLevel ? $newLevel->pos : 0);
// Filter: Nur User die noch nicht auf das neue Level umgestellt wurden
if ($onlyNotUpdated) {
if (!$currentUser || $currentUser->m_level == $newLevelId) {
continue; // Skip - User ist bereits auf das neue Level umgestellt
// Skip wenn:
// 1. User existiert nicht oder hat kein Level
// 2. User ist bereits auf dem neuen Level (gleiche ID)
// 3. User hat bereits ein höheres oder gleichwertiges Level (POS >= neue Level POS)
if (
!$currentUser ||
$currentUser->m_level == $newLevelId ||
($currentUserLevel && $currentUserLevel->pos >= $newLevelPos)
) {
continue; // Skip - User ist bereits auf das neue Level umgestellt oder hat bereits ein höheres Level
}
}
@ -100,7 +114,7 @@ class LevelReportService
'to_level_pos' => $newLevelData['pos'] ?? 0,
'current_user_level_id' => $currentUser ? $currentUser->m_level : null,
'current_user_level_name' => $currentUserLevelName,
'level_updated' => $onlyNotUpdated ? 'Nein' : ($currentUser && $currentUser->m_level == $newLevelId ? 'Ja' : 'Nein'),
'level_updated' => $onlyNotUpdated ? 'Nein' : ($currentUser && ($currentUser->m_level == $newLevelId || ($currentUserLevel && $currentUserLevel->pos >= $newLevelPos)) ? 'Ja' : 'Nein'),
'total_pp' => $userBusiness->total_pp ?? 0,
'total_qual_pp' => $userBusiness->total_qual_pp ?? 0,
'payline_points_qual_kp' => $userBusiness->payline_points_qual_kp ?? 0,
@ -155,7 +169,7 @@ class LevelReportService
return $stats;
}
public function exportToCsv(Collection $promotions, string $filename = null): string
public function exportToCsv(Collection $promotions, ?string $filename = null): string
{
if (!$filename) {
$filename = 'level_promotions_' . date('Y-m-d_H-i-s') . '.csv';
@ -229,4 +243,100 @@ class LevelReportService
return $filepath;
}
/**
* Holt Level-Aufstiege für ein Team basierend auf TreeCalcBotOptimized
* Nur für einen spezifischen Monat/Jahr und nur für Team-Mitglieder
*/
public function getTeamLevelPromotions(TreeCalcBotOptimized $treeCalcBot, int $month, int $year, array $filters = []): Collection
{
$onlyNotUpdated = $filters['only_not_updated'] ?? false;
// Lade UserLevels für Referenz
$userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
// Extrahiere alle User-IDs aus dem Team
$teamUserIds = $this->extractTeamUserIds($treeCalcBot);
if (empty($teamUserIds)) {
return collect([]);
}
// Lade UserBusiness Einträge für Team-Mitglieder mit Level-Aufstiegen
$userBusinesses = UserBusiness::whereIn('user_id', $teamUserIds)
->where('month', $month)
->where('year', $year)
->whereNotNull('next_qual_user_level')
->whereRaw("JSON_LENGTH(next_qual_user_level) > 0")
->orderBy('user_id')
->get();
return $this->processLevelPromotions($userBusinesses, $userLevels, $onlyNotUpdated);
}
/**
* Extrahiert alle User-IDs aus TreeCalcBotOptimized-Struktur
* Ähnlich wie getTeamUsersFromStructure im TeamController, aber nur IDs
*/
private function extractTeamUserIds(TreeCalcBotOptimized $treeCalcBot): array
{
$userIds = [];
$processedIds = [];
// Sammle User-IDs aus Root-Items
$businessUsers = $treeCalcBot->getItems();
foreach ($businessUsers as $businessUser) {
$userId = $businessUser->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
$this->collectUserIdsRecursive($businessUser->businessUserItems ?? [], $userIds, $processedIds);
}
}
// Sammle parentless User-IDs
if ($treeCalcBot->isParentless()) {
$parentless = $treeCalcBot->__get('parentless');
if (is_array($parentless)) {
foreach ($parentless as $businessUser) {
if ($businessUser) {
$userId = $businessUser->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
$this->collectUserIdsRecursive($businessUser->businessUserItems ?? [], $userIds, $processedIds);
}
}
}
}
}
return $userIds;
}
/**
* Sammelt rekursiv User-IDs aus businessUserItems
*/
private function collectUserIdsRecursive(array $businessUserItems, array &$userIds, array &$processedIds, int $depth = 0): void
{
$maxDepth = 20;
if ($depth > $maxDepth) {
return;
}
foreach ($businessUserItems as $businessUserItem) {
if ($businessUserItem) {
$userId = $businessUserItem->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
// Rekursiv für verschachtelte Items
if (isset($businessUserItem->businessUserItems) && is_array($businessUserItem->businessUserItems)) {
$this->collectUserIdsRecursive($businessUserItem->businessUserItems, $userIds, $processedIds, $depth + 1);
}
}
}
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Mail\MailLog;
@ -10,7 +11,8 @@ class MyLog
public static function writeLog($channel = 'payment', $context = 'error', $message = '', $data = []){
public static function writeLog($channel = 'payment', $context = 'error', $message = '', $data = [], $mail = true)
{
switch ($context) {
case 'notice':
@ -26,9 +28,8 @@ class MyLog
\Log::channel($channel)->error($message . ' : ' . json_encode($data));
break;
}
if ($mail) {
Mail::to(config('app.exception_mail'))->send(new MailLog($channel, $context, $message, $data));
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\ShoppingUser;
@ -8,7 +9,8 @@ use Yard;
class OrderPaymentService
{
public static function deleteInstance($identifier){
public static function deleteInstance($identifier)
{
Yard::instance('shopping')->deleteStoredCart($identifier);
\App\Models\ShoppingInstance::where('identifier', $identifier)->delete();
@ -20,7 +22,8 @@ class OrderPaymentService
}*/
}
public static function updateInstanceStatus($identifier, $status, $lower = true){
public static function updateInstanceStatus($identifier, $status, $lower = true)
{
if (!ShoppingInstance::where('identifier', $identifier)->exists()) {
return false;
}
@ -33,7 +36,8 @@ class OrderPaymentService
}
}
public static function getInstanceStatus($identifier){
public static function getInstanceStatus($identifier)
{
$shopping_instance = ShoppingInstance::where('identifier', $identifier)->first();
if (!$shopping_instance) {
return false;
@ -41,7 +45,8 @@ class OrderPaymentService
return $shopping_instance->getStatus();
}
public static function getTypeBadge(ShoppingInstance $shoppingInstance){
public static function getTypeBadge(ShoppingInstance $shoppingInstance)
{
$isFor = $shoppingInstance->shopping_data['is_for'] ?? '-';
if ($isFor === 'abo-ot-customer') {
return ' <span class="badge badge-pill badge-warning">' . __('abo.abo') . '</span>';
@ -52,21 +57,23 @@ class OrderPaymentService
return "";
}
public static function getStatusBadge(ShoppingInstance $shoppingInstance){
public static function getStatusBadge(ShoppingInstance $shoppingInstance)
{
$status = $shoppingInstance->getStatus();
$badgeClasses = [
'link_sent' => 'success',
'link_openly' => 'warning',
'link_sent' => 'info',
'link_openly' => 'info',
'link_paid' => 'secondary',
'link_check' => 'warning',
'link_pending' => 'warning',
'link_appointed' => 'secondary',
'link_appointed' => 'warning',
'link_failed' => 'danger',
'link_canceled' => 'danger'
];
if (isset($badgeClasses[$status])) {
return sprintf(' <span class="badge badge-pill badge-%s">%s</span>',
return sprintf(
' <span class="badge badge-pill badge-%s">%s</span>',
$badgeClasses[$status],
__('payment.' . $status)
);
@ -75,21 +82,23 @@ class OrderPaymentService
return '';
}
public static function getStatusAlert($status){
public static function getStatusAlert($status)
{
$badgeClasses = [
'link_sent' => 'success',
'link_openly' => 'success',
'link_sent' => 'info',
'link_openly' => 'info',
'link_check' => 'warning',
'link_pending' => 'warning',
'link_failed' => 'danger',
'link_canceled' => 'danger',
'link_appointed' => 'success',
'link_appointed' => 'warning',
'link_paid' => 'success',
];
if (isset($badgeClasses[$status])) {
return sprintf(' <div class="alert alert-%s">%s</div>',
return sprintf(
' <div class="alert alert-%s">%s</div>',
$badgeClasses[$status],
__('payment.alert_' . $status)
);
@ -98,7 +107,8 @@ class OrderPaymentService
return '';
}
public static function getCustomPayment($identifier){
public static function getCustomPayment($identifier)
{
$shopping_instance = ShoppingInstance::where('identifier', $identifier)->first();
if (!$shopping_instance) {
@ -123,7 +133,8 @@ class OrderPaymentService
return $data;
}
public static function getRestoredYardShoppingItems($shopping_instance){
public static function getRestoredYardShoppingItems($shopping_instance)
{
Yard::instance('shopping')->destroy();
Yard::instance('shopping')->restore($shopping_instance->identifier, [], false);
@ -176,5 +187,4 @@ class OrderPaymentService
return $ret;
}
}

View file

@ -1,20 +1,22 @@
<?php
namespace App\Services;
use App\User;
use App\Models\UserLevel;
use App\Mail\MailCheckout;
use App\Services\UserUtil;
use App\Services\AboHelper;
use App\Models\ProductBuying;
use App\Models\ShoppingOrder;
use App\Models\UserCreditItem;
use App\Models\ShoppingPayment;
use App\Services\ShopApiOrderCart;
use Illuminate\Support\Facades\Mail;
use App\Models\UserCreditItem;
use App\Models\UserLevel;
use App\Repositories\InvoiceRepository;
use App\Services\AboHelper;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\ShopApiOrderCart;
use App\Services\UserUtil;
use App\Services\Util;
use App\User;
use Illuminate\Support\Facades\Mail;
class Payment
{
@ -61,21 +63,24 @@ class Payment
];
public static function getFormattedTxaction($txaction){
public static function getFormattedTxaction($txaction)
{
if ($txaction && isset(self::$txaction_text[$txaction])) {
return __('payment.' . self::$txaction_text[$txaction]);
}
return __('payment.' . self::$txaction_text['NULL']);
}
public static function getFormattedTxactionColor($txaction){
public static function getFormattedTxactionColor($txaction)
{
if ($txaction && isset(self::$txaction_color[$txaction])) {
return self::$txaction_color[$txaction];
}
return "warning";
}
public static function getTransTxactionFilterText(){
public static function getTransTxactionFilterText()
{
$ret = [];
foreach (self::$txaction_filter_text as $key => $val) {
$ret[$key] = trans('payment.' . $val);
@ -83,7 +88,8 @@ class Payment
return $ret;
}
public static function getTransTxactionInvoice(){
public static function getTransTxactionInvoice()
{
$ret = [];
foreach (self::$txaction_invoice as $key => $val) {
$ret[$key] = trans('payment.' . $val);
@ -91,7 +97,8 @@ class Payment
return $ret;
}
public static function getShoppingOrderBadge(ShoppingOrder $shopping_order){
public static function getShoppingOrderBadge(ShoppingOrder $shopping_order)
{
if ($shopping_order->mode === 'test') {
return '<span class="badge badge-pill badge-default">' . strtoupper($shopping_order->mode) . ' - ' . self::getFormattedTxaction($shopping_order->txaction) . '</span>';
@ -102,7 +109,8 @@ class Payment
return '<span class="badge badge-pill badge-' . self::getFormattedTxactionColor($shopping_order->txaction) . '">' . self::getFormattedTxaction($shopping_order->txaction) . '</span>';
}
public static function getPaymentForBadge(ShoppingOrder $shopping_order){
public static function getPaymentForBadge(ShoppingOrder $shopping_order)
{
$abo = '';
if ($shopping_order->is_abo) {
$abo = ' <span class="badge badge-pill badge-success">' . __('abo.abo') . '</span>';
@ -110,14 +118,16 @@ class Payment
return '<span class="badge badge-pill badge-' . $shopping_order->getPaymentForColor() . '">' . $shopping_order->getPaymentForType() . '</span>' . $abo;
}
public static function getShoppingPaymentBadge(ShoppingPayment $shopping_payment){
public static function getShoppingPaymentBadge(ShoppingPayment $shopping_payment)
{
if ($shopping_payment->mode === 'test') {
return '<span class="badge badge-pill badge-default">' . strtoupper($shopping_payment->mode) . ' - ' . self::getFormattedTxaction($shopping_payment->txaction) . '</span>';
}
return '<span class="badge badge-pill badge-' . self::getFormattedTxactionColor($shopping_payment->txaction) . '">' . self::getFormattedTxaction($shopping_payment->txaction) . '</span>';
}
public static function addUserCreditMargin(User $user, $credit, $status, $message){
public static function addUserCreditMargin(User $user, $credit, $status, $message)
{
UserCreditItem::create([
'user_id' => $user->id,
'credit' => $credit,
@ -128,7 +138,8 @@ class Payment
]);
}
public static function addBuyingRestriction(User $user, $product_id){
public static function addBuyingRestriction(User $user, $product_id)
{
ProductBuying::create([
'user_id' => $user->id,
'product_id' => $product_id,
@ -136,7 +147,8 @@ class Payment
]);
}
public static function addSponsorBuyingPoints(User $user, $product){
public static function addSponsorBuyingPoints(User $user, $product)
{
if ($user->user_sponsor) {
$data = [
@ -151,7 +163,8 @@ class Payment
}
}
public static function updateUserLevel(User $user, $to_level_id){
public static function updateUserLevel(User $user, $to_level_id)
{
//nur updaten, wenn der user->m_level kleiner ist als $to_level_id
if ($user->user_level) {
$ToUserLevel = UserLevel::find($to_level_id);
@ -169,7 +182,8 @@ class Payment
$paid = Status der Zahlung, Payone = true, MIVITA Rechnung = false damit kann später die rechnung auf bezahlt gesetzt werden.
*/
public static function paymentStatusPaidAction(ShoppingOrder $shopping_order, $paid, $shopping_payment = null){
public static function paymentStatusPaidAction(ShoppingOrder $shopping_order, $paid, $shopping_payment = null)
{
$send_link = false;
$shopping_order->setUserHistoryValue(['status' => 8]);
ShoppingUserService::snycOrdersByShoppingOrder($shopping_order);
@ -232,7 +246,6 @@ class Payment
}
}
}
}
}
@ -253,11 +266,11 @@ class Payment
$shopping_payment->identifier = null;
$shopping_payment->save();
}
AboHelper::setAboActive($shopping_order, 2);
AboHelper::setAboActive($shopping_order, 2, true);
}
//make Invoice is not exist and is live
if($shopping_order->mode === 'live'){
if ($shopping_order->mode === 'live' || Util::isTestSystem(true)) {
$invoice_repo = new InvoiceRepository($shopping_order);
if (!$shopping_order->isInvoice()) {
$invoice_repo->createAndSalesVolume();
@ -267,7 +280,8 @@ class Payment
return $send_link;
}
public static function paymentStatusSendMail(ShoppingOrder $shopping_order, $shopping_payment, $data){
public static function paymentStatusSendMail(ShoppingOrder $shopping_order, $shopping_payment, $data)
{
$bcc = [];
$billing_email = $shopping_order->shopping_user->billing_email;

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use Yard;
@ -15,12 +16,14 @@ class PaymentHelper
public function setProduct($product){
public function setProduct($product)
{
Yard::instance('shopping')->destroy();
Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, $product->price, false, false, ['image' => "", 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
}
public function initELVPayment($user){
public function initELVPayment($user)
{
$shopping_user = $this->makeShoppingUser($user);
$shopping_order = $this->makeShoppingOrder($user, $shopping_user);
@ -79,7 +82,8 @@ class PaymentHelper
}
}
public function makeShoppingUser($user, $is_from = 'membership', $is_for = 'me'){
public function makeShoppingUser($user, $is_from = 'membership', $is_for = 'me')
{
$shopping_user = new ShoppingUser();
$shopping_user->auth_user_id = $user->id;
$shopping_user->mode = 'prev';
@ -112,11 +116,13 @@ class PaymentHelper
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->save();
return $shopping_user;
}
public function makeShoppingOrder($user, $shopping_user){
public function makeShoppingOrder($user, $shopping_user)
{
$data = [
'shopping_user_id' => $shopping_user->id,
@ -165,6 +171,4 @@ class PaymentHelper
$shopping_order->makeTaxSplit();
return $shopping_order;
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\ShoppingCollectOrder;
@ -26,10 +27,10 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->tax_split = [];
$this->shoppingCollectOrder->orders = [];
$this->shoppingCollectOrder->shop_items = [];
}
public function add(ShoppingOrder $shopping_order){
public function add(ShoppingOrder $shopping_order)
{
$order = new stdClass();
$order->order_id = $shopping_order->id;
@ -56,7 +57,7 @@ class ShopApiOrderCart
if ($shop_item instanceof stdClass) {
$shop_item->user_price_total_net += $user_price_net_qty;
$shop_item->user_tax_total += $user_tax_qty;
$shop_item->points_total += ($item->points * $item->qty);
$shop_item->points_total += round($item->points * $item->qty, 2);
$shop_item->qty += $item->qty;
}
} else {
@ -69,12 +70,12 @@ class ShopApiOrderCart
$shop_item->qty = $item->qty;
$shop_item->tax_rate = $tax_rate;
$shop_item->points = $item->points;
$shop_item->points = round($item->points, 2);
$shop_item->user_price_net = round($user_price_net, 2);
$shop_item->user_price_total_net = round($user_price_net_qty, 2);
$shop_item->user_tax = round($user_tax, 2);
$shop_item->user_tax_total = round($user_tax_qty, 2);
$shop_item->points_total = ($item->points * $item->qty);
$shop_item->points_total = round($item->points * $item->qty, 2);
}
//only for tax split / tax and price on calculate function
@ -83,7 +84,7 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->addNetToSplit((int)$tax_rate, (float)$user_price_net_qty);
$this->shoppingCollectOrder->tax_total += $user_tax_qty;
$this->shoppingCollectOrder->price_total_net += $user_price_net_qty;
$this->shoppingCollectOrder->points += ($item->points * $item->qty);
$this->shoppingCollectOrder->points += round($item->points * $item->qty, 2);
$this->shoppingCollectOrder->qty_total += $item->qty;
$this->shoppingCollectOrder->addShopItem($shop_item_id, $shop_item);
@ -91,7 +92,8 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->addOrder($order);
}
public function calculate(){
public function calculate()
{
$this->shoppingCollectOrder->shipping_tax = round($this->shoppingCollectOrder->shipping - $this->shoppingCollectOrder->shipping_net, 2);
$this->shoppingCollectOrder->tax_total += $this->shoppingCollectOrder->shipping_tax;
@ -103,7 +105,8 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->price_total = round($this->shoppingCollectOrder->tax_total + $this->shoppingCollectOrder->price_total_net, 2);
}
public function store(){
public function store()
{
$this->shoppingCollectOrder->user_id = \Auth::user()->id;
$this->shoppingCollectOrder->status = 1;
//remove shopping_order
@ -117,7 +120,8 @@ class ShopApiOrderCart
}
//price brutto calu with
private function calcuPriceWith($price, $tax_rate = null, $discount = null){
private function calcuPriceWith($price, $tax_rate = null, $discount = null)
{
$tax_dec = ($tax_rate + 100) / 100;
$price = $price / $tax_dec;
$margin = (($discount - 100) * -1) / 100;
@ -125,13 +129,15 @@ class ShopApiOrderCart
return round($price, 2);
}
private function calcuTaxWith($price, $tax_rate = null){
private function calcuTaxWith($price, $tax_rate = null)
{
$tax_dec = ($tax_rate + 100) / 100;
$tax = ($price * $tax_dec) - $price;
return round($tax, 2);
}
private function setOrderAdress($from, $shopping_user){
private function setOrderAdress($from, $shopping_user)
{
$ret = "";
if ($from === 'billing') {
$ret .= $shopping_user->billing_company ? 'Firma: ' . $shopping_user->billing_company . ' | ' : '';
@ -143,7 +149,6 @@ class ShopApiOrderCart
$ret .= $shopping_user->billing_city . ' | ';
$ret .= $shopping_user->billing_country->getLocated() . ' | ';
$ret .= $shopping_user->billing_email;
}
if ($from === 'shipping') {
if ($shopping_user->same_as_billing == 1) {
@ -157,13 +162,13 @@ class ShopApiOrderCart
$ret .= $shopping_user->shipping_zipcode . ' ';
$ret .= $shopping_user->shipping_city . ' | ';
$ret .= $shopping_user->shipping_country->getLocated() . ' | ';
}
return $ret;
}
public function __get($property) {
public function __get($property)
{
if (property_exists($this->shoppingCollectOrder, $property)) {
return $this->shoppingCollectOrder->$property;
}
@ -192,7 +197,8 @@ class ShopApiOrderCart
return 0;
}
public static function finishOrder(ShoppingCollectOrder $shoppingCollectOrder){
public static function finishOrder(ShoppingCollectOrder $shoppingCollectOrder)
{
//get orders an set
foreach ($shoppingCollectOrder->orders as $order) {
@ -207,5 +213,4 @@ class ShopApiOrderCart
$shoppingCollectOrder->status = 2; //order
$shoppingCollectOrder->save();
}
}

View file

@ -1,12 +1,14 @@
<?php
namespace App\Services;
use App\User;
use App\Models\ShoppingUser;
use App\Models\UserCleanUpLog;
use App\Models\ShoppingUserMemberLog;
use App\Http\Controllers\Api\KasController;
use App\Models\ShoppingUser;
use App\Models\ShoppingUserMemberLog;
use App\Models\UserCleanUpLog;
use App\Models\UserShop;
use App\User;
class UserUtil
{
@ -26,7 +28,8 @@ class UserUtil
}
}
public static function setNewSponsorToChilds($inactive_sponsor_id, $new_sponsor_id){
public static function setNewSponsorToChilds($inactive_sponsor_id, $new_sponsor_id)
{
//alle User die diesen inaktivien Sponsor haben
$child_users = User::where('m_sponsor', $inactive_sponsor_id)->get(); //auch deaktiverte
foreach ($child_users as $child_user) {
@ -40,7 +43,8 @@ class UserUtil
}
}
public static function resetChildsToSponsor($re_sponsor_id){
public static function resetChildsToSponsor($re_sponsor_id)
{
//alle alten Childs vom re_sponsor_id / User wieder herstellen
$UserCleanUpUsers = UserCleanUpLog::where('inactive_sponsor_id', $re_sponsor_id)->get();
foreach ($UserCleanUpUsers as $UserCleanUpUser) {
@ -62,10 +66,10 @@ class UserUtil
$UserCleanUpUser->delete();
}
}
}
public static function setUserToClient($user_id, $sponsor_id){
public static function setUserToClient($user_id, $sponsor_id)
{
$user = User::find($user_id);
if ($user) {
@ -94,6 +98,7 @@ class UserUtil
'shipping_city' => $user->account->shipping_city,
'shipping_country_id' => $user->account->shipping_country_id,
'shipping_phone' => $user->account->getShippingPhoneFull(),
'shipping_postnumber' => $user->account->shipping_postnumber,
];
ShoppingUser::create($data);
}
@ -104,7 +109,8 @@ class UserUtil
find next activ sponsor on user id
first $sponsor_id can user_id, looks has m_sponsor or pre_sponsor.
*/
public static function findNextActiveSponsor($sponsor_id){
public static function findNextActiveSponsor($sponsor_id)
{
$user = User::withTrashed()->find($sponsor_id);
if (!$user) { //kein User unter der ID - to root
@ -124,7 +130,8 @@ class UserUtil
return $user;
}
public static function deactiveUser($user){
public static function deactiveUser($user)
{
$user->pre_sponsor = $user->m_sponsor; //den sponsor speichern für wiederherstellung
$user->m_sponsor = null;
@ -132,8 +139,8 @@ class UserUtil
$user->save();
}
public static function reactiveUser($user){
public static function reactiveUser($user)
{
if ($user->pre_sponsor) {
$pre_sponsor = self::findNextActiveSponsor($user->pre_sponsor);
$user->m_sponsor = $pre_sponsor->id; //den sponsor wiederherstellen
@ -147,19 +154,19 @@ class UserUtil
{
//shop wird gelöscht
if ($user->shop) {
$subdomain_name = $user->shop->slug.'.mivita.care';
// $subdomain_name = $user->shop->slug . '.mivita.care';
$user->shop->name = "delete" . $user->shop->id;
$user->shop->slug = "delete" . $user->shop->id;
$user->shop->save();
$user->shop->delete();
//isset KAS - delete Subdomain
if(!Util::isTestSystem()){
/*if (!Util::isTestSystem()) {
$kas = new KasController();
$pra = array(
'subdomain_name' => $subdomain_name,
);
$kas->action('delete_subdomain', $pra);
}
}*/
}
//user soll nicht komplett gelöscht werden
@ -191,8 +198,53 @@ class UserUtil
return true;
}
public static function checkEmailExists($user)
{
$email = str_replace("delete-", "", $user->email);
public static function reactiveUserResetChilds($user_id, $info = ''){
$user = User::where('email', $email)->first();
if ($user) {
return 'Der Account kann nicht wieder hergestellt werden, da die E-Mail-Adresse <b>' . $email . '</b> bereits in Verwendung ist.';
}
return null;
}
public static function restoreUser($user, $payment_account)
{
if ($user->pre_sponsor) {
$pre_sponsor = self::findNextActiveSponsor($user->pre_sponsor);
$user->m_sponsor = $pre_sponsor->id; //den sponsor wiederherstellen
$user->pre_sponsor = null;
}
$user->email = str_replace("delete-", "", $user->email);
$user->confirmed = 1;
$user->confirmation_date = now();
$user->confirmation_code = null;
$user->confirmation_code_to = null;
$user->confirmation_code_remider = 0;
$user->active = 1;
$user->active_date = now();
$user->deleted_at = null;
$user->pre_deleted_at = null;
$user->payment_account = $payment_account;
$user->payment_shop = $payment_account;
$user->wizard = 100;
$user->save();
$userShop = UserShop::withTrashed()->where('user_id', $user->id)->first();
if ($userShop) {
$userShop->name = null;
$userShop->slug = null;
$userShop->active = 0;
$userShop->deleted_at = null;
$userShop->save();
}
}
public static function reactiveUserResetChilds($user_id, $info = '')
{
$user = User::find($user_id);
if (!$user) {
@ -212,7 +264,8 @@ class UserUtil
self::resetChildsToSponsor($user->id);
}
public static function deactiveUserNewSponsorChilds($user_id, $info = ''){
public static function deactiveUserNewSponsorChilds($user_id, $info = '')
{
$user = User::find($user_id);
if (!$user) {
@ -236,6 +289,4 @@ class UserUtil
\Log::channel('cleanup')->info('deactiveUserNewSponsorChilds ' . $info . ' : ' . json_encode($data));
self::deactiveUser($user);
}
}

View file

@ -2,17 +2,13 @@
namespace App\Services;
use App\Models\Country;
use App\Models\ShippingCountry;
use App\Models\UserHistory;
use App\Models\UserShop;
use Illuminate\Support\Str;
use Request;
use Yard;
class Util
{
private static $postRoute = 'base.';
public static function getToken()
@ -23,91 +19,132 @@ class Util
public static function uuidToken()
{
$uuid = (string) Str::uuid();
$e_uuid = explode("-", $uuid);
$e_uuid = explode('-', $uuid);
if (isset($e_uuid[0]) && $e_uuid[1]) {
return $e_uuid[0] . "-" . $e_uuid[1];
return $e_uuid[0] . '-' . $e_uuid[1];
}
return $uuid;
}
public static function formatDate()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'yyyy-mm-dd';
}
return 'dd.mm.yyyy';
}
public static function formatDateDB()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'Y-m-d';
}
return 'd.m.Y';
}
public static function formatDateTimeDB()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'Y-m-d - H:i';
}
return 'd.m.Y - H:i';
}
public static function _format_number($value)
{
return preg_replace("/[^0-9,-]/", "", $value);
// Erlaubt Zahlen, Komma, Punkt und Minus (für Dezimalzahlen in DE und EN Format)
return preg_replace('/[^0-9,.-]/', '', $value);
}
public static function _thousands_separator()
{
return \App::getLocale() === "en" ? ',' : '.';
return \App::getLocale() === 'en' ? ',' : '.';
}
public static function _decimal_separator()
{
return \App::getLocale() === "en" ? '.' : ',';
return \App::getLocale() === 'en' ? '.' : ',';
}
public static function maxStrLength($str, $length = 40)
{
if (strlen($str) > $length) {
$str = substr($str, 0, $length);
// $str = substr($str, 0, strrpos($str, " "));
$str = $str . " ...";
$str = $str . ' ...';
}
return $str;
}
public static function reFormatNumber($value)
{
return (float) str_replace(',', '.', self::_format_number($value));
// Wenn bereits ein Float/Int, direkt zurückgeben
if (is_numeric($value) && ! is_string($value)) {
return (float) $value;
}
$value = (string) $value;
// Entferne alle nicht-numerischen Zeichen außer Punkt, Komma und Minus
$value = preg_replace('/[^0-9,.\-]/', '', $value);
if ($value === '') {
return 0.0;
}
// Erkenne das Zahlenformat anhand der Position des letzten Trennzeichens
$lastComma = strrpos($value, ',');
$lastDot = strrpos($value, '.');
if ($lastComma !== false && ($lastDot === false || $lastComma > $lastDot)) {
// Deutsches Format: "1.000,50" -> Punkt entfernen (Tausender), Komma zu Punkt
$value = str_replace('.', '', $value);
$value = str_replace(',', '.', $value);
} else {
// Englisches Format: "1,000.50" -> Komma entfernen (Tausender)
$value = str_replace(',', '', $value);
}
return (float) $value;
}
public static function formatNumber($value, $dec = 2)
{
$value = floatval(str_replace(',', '', $value));
// Wenn der Wert bereits numerisch ist (int/float), direkt formatieren
if (is_numeric($value) && ! is_string($value)) {
return number_format((float) $value, $dec, self::_decimal_separator(), self::_thousands_separator());
}
// Bei String-Eingaben: deutsches Format (mit Komma) zu Float konvertieren
$value = self::reFormatNumber($value);
return number_format($value, $dec, self::_decimal_separator(), self::_thousands_separator());
}
public static function cleanIntegerFromString($value)
{
// Entferne alle nicht-numerischen Zeichen außer Minus
$cleanStr = preg_replace("/[^0-9-]/", "", $value);
$cleanStr = preg_replace('/[^0-9-]/', '', $value);
// Konvertiere zu Integer und entferne führende Nullen
$number = (int) $cleanStr;
return $number;
}
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false)
{
if ($fullzero && $num == 0) {
return number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator());
}
return rtrim(rtrim(number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator()), '0'), self::_decimal_separator());
}
@ -118,16 +155,17 @@ class Util
$mixed[$key] = self::utf8ize($value);
}
} elseif (is_string($mixed)) {
return mb_convert_encoding($mixed, "UTF-8", "UTF-8");
}
return $mixed;
return mb_convert_encoding($mixed, 'UTF-8', 'UTF-8');
}
return $mixed;
}
public static function getPostRoute()
{
return self::$postRoute;
}
public static function setPostRoute($postRoute)
{
self::$postRoute = $postRoute;
@ -139,6 +177,7 @@ class Util
if (empty($shop) || ! is_object($shop)) {
return null;
}
return $shop;
}
@ -148,6 +187,7 @@ class Util
if ($user && $user->shop) {
return $user->shop;
}
return false;
}
@ -158,10 +198,10 @@ class Util
return $auth_user;
}
}
return false;
}
public static function getUserShopIdentifier()
{
if (\Session::has('user_shop_identifier')) {
@ -169,6 +209,7 @@ class Util
return $user_shop_identifier;
}
}
return false;
}
@ -178,6 +219,7 @@ class Util
if ($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6) {
return OrderPaymentService::getInstanceStatus($identifier);
}
return false;
}
@ -201,6 +243,7 @@ class Util
if (\Session::has('shopping_instance')) {
return \Session::get('shopping_instance');
}
return false;
}
@ -211,6 +254,7 @@ class Util
if ($user_shop_identifier && $auth_user) {
return UserHistory::whereUserId($auth_user->id)->whereIdentifier($user_shop_identifier)->get()->last();
}
return false;
}
@ -229,6 +273,7 @@ class Util
if ($user_history = self::getUserHistory()) {
return $user_history->{$key};
}
return null;
}
@ -239,8 +284,10 @@ class Util
return 'test';
}
}
return config('app.mode');
}
public static function addRoute($p = [])
{
$b = [];
@ -249,6 +296,7 @@ class Util
$b = ['subdomain' => $user_shop->slug];
}
}
return array_merge($p, $b);
}
@ -260,8 +308,10 @@ class Util
if ($user->account->country_id === 6) {
return false;
}
return true;
}
return false;
}
@ -272,34 +322,38 @@ class Util
return \Auth::guard('customers')->user()->user_shop_domain;
}
}
return self::getMyMivitaShopUrl();
}
public static function getMyMivitaShopUrl($add_url = "")
public static function getMyMivitaShopUrl($add_url = '')
{
if (\Session::has('user_shop_domain')) {
$url = \Session::get('user_shop_domain') . $add_url;
if (! str_starts_with($url, 'http')) {
$url = 'https://' . ltrim($url, '/');
}
return $url;
}
// alois sein shop
$user = \App\User::find(6);
if ($user && $user->shop) {
return config('app.protocol') . $user->shop->slug . "." . config('app.domain') . config('app.tld_care') . $add_url;
return config('app.protocol') . $user->shop->slug . '.' . config('app.domain') . config('app.tld_care') . $add_url;
}
}
public static function getMyMivitaPortalUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
$pro = $protocol ? config('app.protocol') : '';
return $pro . config('app.pre_url_portal') . config('app.domain') . config('app.tld_care');
}
public static function getMyMivitaUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
$pro = $protocol ? config('app.protocol') : '';
return $pro . config('app.pre_url_crm') . config('app.domain') . config('app.tld_care');
}
@ -311,10 +365,11 @@ class Util
if (\Session::has('user_shop_payment')) {
return \Session::get('user_shop_payment');
}
return null;
}
public static function getUserShopBackUrl($reference = "")
public static function getUserShopBackUrl($reference = '')
{
if (\Session::has('user_shop')) {
@ -322,9 +377,10 @@ class Util
return \Session::get('user_shop_domain');
}
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . "/back/to/shop/" . $reference;
return config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') . '/back/to/shop/' . $reference;
}
}
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
@ -337,17 +393,19 @@ class Util
return \Session::get('back_link');
}
if (self::getUserPaymentFor($instance) === 3) {
return \Session::get('user_shop_domain') . "/user/membership";
return \Session::get('user_shop_domain') . '/user/membership';
}
if (self::getUserPaymentFor($instance) === 2) {
return \Session::get('user_shop_domain') . "/user/orders";
return \Session::get('user_shop_domain') . '/user/orders';
}
return \Session::get('user_shop_domain');
}
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . $uri;
return config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') . $uri;
}
}
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
@ -363,6 +421,7 @@ class Util
if (Request::getHost() === 'naturcosmetic.' . config('app.domain') . config('app.tld_care')) {
return true;
}
return \Config::get('app.url') === config('app.domain') . config('app.tld_shop');
}
@ -372,8 +431,10 @@ class Util
if ($dev && config('app.debug') !== true) {
return false;
}
return true;
}
return false;
}
@ -382,7 +443,7 @@ class Util
if ($size > 0) {
$size = (int) $size;
$base = log($size) / log(1024);
$suffixes = array(' bytes', ' KB', ' MB', ' GB', ' TB');
$suffixes = [' bytes', ' KB', ' MB', ' GB', ' TB'];
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
} else {
@ -390,56 +451,70 @@ class Util
}
}
public static function formatTextWithLineBreaks($text, $translate = false)
{
if ($translate) {
$translated = ['payment.commission_shop', 'payment.commission_payline', 'payment.commission_growth_bonus'];
foreach ($translated as $value) {
// $text = str_replace($key, trans($value), $text);
$text = str_replace($value, __($value), $text);
}
}
return nl2br($text);
}
public static function sanitize($string, $force_lowercase = true, $anal = false, $substr = false)
{
$strip = array(
"~",
"`",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"_",
"=",
"+",
"[",
"{",
"]",
"}",
"\\",
"|",
";",
":",
"\"",
$strip = [
'~',
'`',
'!',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'_',
'=',
'+',
'[',
'{',
']',
'}',
'\\',
'|',
';',
':',
'"',
"'",
"&#8216;",
"&#8217;",
"&#8220;",
"&#8221;",
"&#8211;",
"&#8212;",
"—",
"–",
",",
"<",
".",
">",
"/",
"?"
);
$clean = trim(str_replace($strip, "", strip_tags($string)));
$clean = preg_replace('/\s+/', "_", $clean);
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean;
'&#8216;',
'&#8217;',
'&#8220;',
'&#8221;',
'&#8211;',
'&#8212;',
'—',
'–',
',',
'<',
'.',
'>',
'/',
'?',
];
$clean = trim(str_replace($strip, '', strip_tags($string)));
$clean = preg_replace('/\s+/', '_', $clean);
$clean = ($anal) ? preg_replace('/[^a-zA-Z0-9]/', '', $clean) : $clean;
if ($substr) {
$clean = (strlen($clean) > 20) ? substr($clean, -20) : $clean;
}
return ($force_lowercase) ?
(function_exists('mb_strtolower')) ?
mb_strtolower($clean, 'UTF-8') :

View file

@ -1,39 +1,50 @@
<?php
namespace App\Services;
use App\Services\Shop;
use App\Models\Country;
use App\Models\Product;
use App\Models\ShippingCountry;
use \Gloudemans\Shoppingcart\Cart;
use Illuminate\Support\Collection;
use Gloudemans\Shoppingcart\Cart;
use Gloudemans\Shoppingcart\CartItem;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Events\Dispatcher;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Session\SessionManager;
use Illuminate\Support\Collection;
class Yard extends Cart
{
private $shipping_price = 0;
private $shipping_price_net = 0;
private $shipping_tax_rate = 0;
private $shipping_tax = 0;
private $shipping_country_id = 0; //default de
private $shipping_is_for;
private $num_comp;
private $user_tax_free;
private $shipping_free;
private $user_reverse_charge;
private $user_country_id;
private $user_country;
private $shopping_data = [];
private $initShippingExtras = false;
private $shipping_price_net = 0;
private $shipping_tax_rate = 0;
private $shipping_tax = 0;
private $shipping_country_id = 0; // default de
private $shipping_is_for;
private $num_comp;
private $user_tax_free;
private $shipping_free;
private $user_reverse_charge;
private $user_country_id;
private $user_country;
private $shopping_data = [];
private $initShippingExtras = false;
public function __construct(SessionManager $session, Dispatcher $events)
{
parent::__construct($session, $events);
}
public function instance($instance = null)
@ -47,7 +58,8 @@ class Yard extends Cart
return $this;
}
private function makeShippingExtras($instance){
private function makeShippingExtras($instance)
{
if ($this->getYardExtra('shipping_price')) {
$this->shipping_price = (float) ($this->getYardExtra('shipping_price'));
@ -102,18 +114,21 @@ class Yard extends Cart
return config('cart.tax');
}
public function putYardExtra($key, $value){
public function putYardExtra($key, $value)
{
$content = $this->getYContent();
$content->put($key, $value);
$this->putShippingExtras($content);
// $this->ysession->put($this->yinstance, $content);
}
public function getYardExtra($key){
public function getYardExtra($key)
{
$content = $this->getYContent();
if ($content->has($key)) {
return $content->get($key);
}
return false;
}
@ -126,20 +141,24 @@ class Yard extends Cart
return $this->ysession->get($this->yinstance);*/
}
public function getShippingCountryName(){
public function getShippingCountryName()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if ($shippingCountry && $shippingCountry->country) {
return $shippingCountry->country->getLocated();
}
return "";
return '';
}
public function getShippingCountryCountryId()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if ($shippingCountry && $shippingCountry->country) {
return $shippingCountry->country->id;
}
return 1; // default DE
}
@ -148,17 +167,13 @@ class Yard extends Cart
return $this->shipping_country_id;
}
public function getShippingPrice()
{
return $this->shipping_price;
}
public function reCalculateShippingPrice(){
public function reCalculateShippingPrice()
{
$this->calculateShippingPrice();
}
@ -171,7 +186,6 @@ class Yard extends Cart
$this->putYardExtra('shipping_is_for', $shipping_is_for);
$this->calculateShippingPrice();
}
public function setUserPriceInfos($user_price_infos = [])
@ -190,10 +204,10 @@ class Yard extends Cart
$this->user_country = Country::findOrFail($user_price_infos['user_country_id']);
$this->putYardExtra('user_country', $this->user_country);
}
public function getUserPriceInfos(){
public function getUserPriceInfos()
{
return [
'user_tax_free' => $this->user_tax_free,
'user_reverse_charge' => $this->user_reverse_charge,
@ -227,11 +241,12 @@ class Yard extends Cart
if ($this->shipping_free && $this->total(2, '.', '') < $this->shipping_free) {
return $this->shipping_free - $this->total(2, '.', '');
}
return 0;
}
private function calculateShippingPrice(){
private function calculateShippingPrice()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if (! $shippingCountry) {
@ -253,7 +268,6 @@ class Yard extends Cart
} else {
$shipping_price = $this->shippingPriceByWeight($shipping->shipping_prices, $this->weightByFreeShipping());
}
} else {
$shipping_price = $this->shippingPriceByWeight($shipping->shipping_prices, $this->weight());
// first by price
@ -273,7 +287,6 @@ class Yard extends Cart
if (Shop::isCompProducts($this->shipping_is_for)) {
$price = $shipping_price->price_comp;
$this->num_comp = $shipping_price->num_comp;
}
$this->shipping_price = $price;
$this->shipping_tax_rate = $shipping_price->tax_rate;
@ -288,7 +301,8 @@ class Yard extends Cart
}
}
private function shippingPriceByTotal($prices, $total){
private function shippingPriceByTotal($prices, $total)
{
foreach ($prices as $price) {
if ($price->total_from > 0 && $price->total_to > 0) {
if ($total >= $price->total_from && $total <= $price->total_to) {
@ -296,9 +310,12 @@ class Yard extends Cart
}
}
}
return false;
}
private function shippingPriceByWeight($prices, $weight){
private function shippingPriceByWeight($prices, $weight)
{
foreach ($prices as $price) {
if ($price->weight_from > 0 && $price->weight_to > 0) {
if ($weight >= $price->weight_from && $weight <= $price->weight_to) {
@ -306,6 +323,7 @@ class Yard extends Cart
}
}
}
return false;
}
@ -324,11 +342,11 @@ class Yard extends Cart
{
return $this->numberFormat($this->shipping_price_net, $decimals, $decimalPoint, $thousandSeperator);
}
//
private function shippingTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
return $this->numberFormat($this->shipping_tax, $decimals, $decimalPoint, $thousandSeperator);
}
/* private function subShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null){
@ -339,10 +357,10 @@ class Yard extends Cart
public function subtotalWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$subtotal = (float) $this->shipping_price_net + $this->subtotal(2, '.', '');
return $this->numberFormat($subtotal, $decimals, $decimalPoint, $thousandSeperator);
}
public function taxWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->user_tax_free) {
@ -351,10 +369,10 @@ class Yard extends Cart
$total = $this->totalWithShipping(2, '.', '');
// $totalTax = (float) $this->tax(2, '.', '') + $this->shipping_tax;
$totalTax = $this->subtotalWithShipping(2, '.', '');
return $this->numberFormat(($total - $totalTax), $decimals, $decimalPoint, $thousandSeperator);
}
public function totalWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->user_tax_free) {
@ -391,8 +409,10 @@ class Yard extends Cart
if ($cartItem->options->no_free_shipping) {
return $total + ($cartItem->options->weight ? ($cartItem->options->weight * $cartItem->qty) : 0);
}
return $total;
}, 0);
return $total;
}
@ -400,7 +420,10 @@ class Yard extends Cart
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->options->points ? ($cartItem->options->points * $cartItem->qty) : 0);
// Punkte als Float konvertieren (falls als String mit Komma gespeichert)
$points = $cartItem->options->points ? Util::reFormatNumber($cartItem->options->points) : 0;
return $total + ($points * $cartItem->qty);
}, 0);
return $total;
@ -414,8 +437,10 @@ class Yard extends Cart
$comp_count = $content->reduce(function ($comp_count, CartItem $cartItem) {
return $cartItem->options->comp ? $comp_count + 1 : $comp_count;
}, 0);
return $count - $comp_count;
}
/**
* Get the total price of the items in the cart.
*
@ -424,7 +449,7 @@ class Yard extends Cart
* @param string $thousandSeperator
* @return string
*/
public function total($decimals = NULL, $decimalPoint = NULL, $thousandSeperator = NULL, $withFees = true)
public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
@ -442,12 +467,13 @@ class Yard extends Cart
* @param string $thousandSeperator
* @return float
*/
public function tax($decimals = NULL, $decimalPoint = NULL, $thousandSeperator = NULL, $withFees = true)
public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$tax = $content->reduce(function ($tax, CartItem $cartItem) {
$priceTax = $cartItem->price / (100 + $cartItem->taxRate) * $cartItem->taxRate;
return $tax + ($cartItem->qty * $priceTax);
}, 0);
@ -468,16 +494,17 @@ class Yard extends Cart
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
$price_net = $cartItem->price / ((100 + $cartItem->taxRate) / 100);
return $subTotal + ($cartItem->qty * $price_net);
}, 0);
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
public function getCartItemByProduct($product_id, $set_price='with'){
public function getCartItemByProduct($product_id, $set_price = 'with')
{
if ($product = Product::find($product_id)) {
$image = "";
$image = '';
if ($product->images->count()) {
$image = $product->images->first()->slug;
}
@ -495,14 +522,15 @@ class Yard extends Cart
if ($content->has($cartItem->rowId)) {
return $content->get($cartItem->rowId);
}
return $cartItem;
}
return null;
return null;
}
public function getCartItem($id, $name = null, $qty = null, $price = null, array $options = []){
public function getCartItem($id, $name = null, $qty = null, $price = null, array $options = [])
{
if ($id instanceof Buyable) {
$cartItem = CartItem::fromBuyable($id, $qty ?: []);
} elseif (is_array($id)) {
@ -510,6 +538,7 @@ class Yard extends Cart
} else {
$cartItem = CartItem::fromAttributes($id, $name, $price, $options);
}
return $cartItem;
}
@ -517,16 +546,19 @@ class Yard extends Cart
{
// $this->ysession->remove($this->yinstance);
parent::destroy();
}
public function rowPriceNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
public function rowPriceNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
return $this->numberFormat($price, $decimals, $decimalPoint, $thousandSeperator);
}
public function rowSubtotalNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
public function rowSubtotalNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
return $this->numberFormat(($price * $row->qty), $decimals, $decimalPoint, $thousandSeperator);
}
@ -540,16 +572,20 @@ class Yard extends Cart
return ($this->user_country && $this->user_country->currency) ? $this->user_country->currency_unit : false;
}
public function convertCurrency($value = 0, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
public function convertCurrency($value = 0, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->isPriceCurrency()) {
$faktor = isset($this->user_country->currency_faktor) ? $this->user_country->currency_faktor : 1;
$value = Util::reFormatNumber($value);
return $this->numberFormat($value, $decimals, $decimalPoint, $thousandSeperator);
}
return '';
return '';
}
public function getCurrencyByKey($key = false, CartItem $row = null, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
public function getCurrencyByKey($key = false, ?CartItem $row = null, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->isPriceCurrency()) {
$rNumber = 0;
@ -592,26 +628,31 @@ class Yard extends Cart
$rNumber = (float) ($this->shipping(2, '.', '')) * $faktor;
break;
}
return $this->numberFormat($rNumber, $decimals, $decimalPoint, $thousandSeperator);
return $this->numberFormat($rNumber, $decimals, $decimalPoint, $thousandSeperator);
}
return '';
}
public function getNumComp(){
public function getNumComp()
{
return $this->num_comp;
}
public function getCompProductBy($comp, $product_id=false){
public function getCompProductBy($comp, $product_id = false)
{
foreach ($this->content() as $row) {
if ($row->options->comp == $comp) {
return $row->options->product_id;
}
}
return false;
}
public function getContentByOrder(){
public function getContentByOrder()
{
$ret = [];
$comp = [];
foreach ($this->content() as $row) {
@ -623,16 +664,13 @@ class Yard extends Cart
}
ksort($comp);
$ret = array_merge($ret, $comp);
return $ret;
}
/**
* Get the Formated number
*
* @param $value
* @param $decimals
* @param $decimalPoint
* @param $thousandSeperator
* @return string
*/
protected function numberFormat($value, $decimals, $decimalPoint, $thousandSeperator)
@ -650,7 +688,6 @@ class Yard extends Cart
return number_format($value, $decimals, $decimalPoint, $thousandSeperator);
}
public function myStore($identifier, array $eventOptions = [])
{
@ -667,7 +704,7 @@ class Yard extends Cart
$this->getConnection()->table($this->getTableName())->insert([
'identifier' => $identifier,
'instance' => $this->currentInstance(),
'content' => serialize($content)
'content' => serialize($content),
]);
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
@ -682,7 +719,4 @@ class Yard extends Cart
* @param mixed $identifier
* @return void
*/
}

View file

@ -605,9 +605,11 @@ class User extends Authenticatable
return 0;
}
public function getUserSalesVolume($month, $year, $record = 'get')
//with = ['shopping_order.shopping_user'] <- optional wenn es noch weitere relations gibt
public function getUserSalesVolume($month, $year, $record = 'get', $with = [])
{
$query = UserSalesVolume::where('user_id', $this->id)->where('month', $month)->where('year', $year)->orderBy('id', 'DESC');
$relations = array_merge(['shopping_order'], $with);
$query = UserSalesVolume::with($relations)->where('user_id', $this->id)->where('month', $month)->where('year', $year)->orderBy('id', 'DESC');
switch ($record) {
case 'get':
return $query->get();

View file

@ -30,7 +30,8 @@ if (! function_exists('get_file_last_time')) {
}
if (! function_exists('get_user_attr')) {
function get_user_attr($key){
function get_user_attr($key)
{
if ($user = Auth::user()) {
return $user->getSetting($key);
}
@ -39,7 +40,8 @@ if (! function_exists('get_user_attr')) {
}
if (! function_exists('set_user_attr')) {
function set_user_attr($key, $value){
function set_user_attr($key, $value)
{
if ($user = Auth::user()) {
return $user->setSetting([$key => $value]);
}
@ -88,32 +90,44 @@ if (! function_exists('formatDate')) {
if (! function_exists('maxStrLength')) {
function maxStrLength($str, $lenght = 40)
{
return !$str ? $str : Util::maxStrLength($str, $lenght); }
return !$str ? $str : Util::maxStrLength($str, $lenght);
}
}
if (! function_exists('get_abo_type_badge_by_product')) {
function get_abo_type_badge_by_product($product){
function get_abo_type_badge_by_product($product)
{
return AboHelper::getAboTypeBadge(AboHelper::getAboShowOn($product));
}
}
if (! function_exists('get_abo_type_badge')) {
function get_abo_type_badge($type){
function get_abo_type_badge($type)
{
return AboHelper::getAboTypeBadge($type);
}
}
if (! function_exists('cleanIntegerFromString')) {
function cleanIntegerFromString($value) {
function cleanIntegerFromString($value)
{
return Util::cleanIntegerFromString($value);
}
}
if (! function_exists('formatTextWithLineBreaks')) {
function formatTextWithLineBreaks($text, $translate = false)
{
return Util::formatTextWithLineBreaks($text, $translate);
}
}
if (! function_exists('legal_url')) {
/**
* Generate URL for legal pages that should always point to shop domain from checkout
*/
function legal_url($path) {
function legal_url($path)
{
try {
// HOTFIX: DomainContext temporär deaktiviert
// TODO: Nach Claude v2 Implementation wieder aktivieren

1489
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -106,13 +106,13 @@ return [
'redis' => [
'client' => 'predis',
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'database' => env('REDIS_DB', 0),
],
],

View file

@ -35,38 +35,51 @@ return [
'channels' => [
'order_controller' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/order_controller.log'),
'level' => 'debug',
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'abo_order' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/abo_order.log'),
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'domain' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/domain.log'),
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'payone' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/payone.log'),
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'cleanup' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/cleanup.log'),
'level' => env('LOG_LEVEL', 'info'),
'days' => 7,
],
'payment' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/payment.log'),
'level' => env('LOG_LEVEL', 'warning'),
'days' => 30,
],
'cron' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/cron.log'),
'level' => env('LOG_LEVEL', 'info'),
'days' => 14,
],
'dhl' => [
'driver' => 'daily',
'path' => storage_path('logs/dhl.log'),
'level' => 'debug',
'level' => env('LOG_LEVEL', 'info'),
'days' => 30,
],
'stack' => [
@ -75,16 +88,17 @@ return [
],
'single' => [
'driver' => 'single',
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 7,
'level' => env('LOG_LEVEL', 'warning'),
'days' => 14,
],
'slack' => [

View file

@ -51,6 +51,7 @@ class CreateUserBusinessesTable extends Migration
$table->decimal('sales_volume_total_shop', 13, 2)->nullable();
$table->decimal('sales_volume_total_sum', 13, 2)->nullable();
$table->integer('calc_qual_kp')->nullable();
$table->integer('payline_points')->nullable();
$table->integer('payline_points_qual_kp')->nullable();

View file

@ -20,6 +20,7 @@ class CreateUserAboOrdersTable extends Migration
$table->unsignedInteger('user_abo_id');
$table->unsignedInteger('shopping_order_id');
$table->unsignedTinyInteger('status')->index()->default(0);
$table->boolean('paid')->default(false);
$table->timestamps();
$table->foreign('user_abo_id')

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDashboardNewsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('dashboard_news', function (Blueprint $table) {
$table->id();
$table->string('title')->nullable();
$table->text('teaser')->nullable();
$table->text('content')->nullable();
$table->json('trans_title')->nullable();
$table->json('trans_teaser')->nullable();
$table->json('trans_content')->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('dashboard_news');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDisplayDateToDashboardNews extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('dashboard_news', function (Blueprint $table) {
$table->date('display_date')->nullable()->after('active');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('dashboard_news', function (Blueprint $table) {
$table->dropColumn('display_date');
});
}
}

View file

@ -0,0 +1,101 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Migration: Points-Spalten von INT auf DECIMAL(10,2) umstellen
*
* Betroffene Tabellen:
* - products: points
* - user_sales_volumes: points, month_KP_points, month_TP_points, month_shop_points
* - user_businesses: sales_volume_KP_points, sales_volume_TP_points, sales_volume_points_shop,
* sales_volume_points_KP_sum, sales_volume_points_TP_sum, payline_points, payline_points_qual_kp
* - shopping_orders: points
* - shopping_order_items: points
*
* Grund: Produkte können Dezimal-Punkte haben (z.B. 12.5 Punkte)
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. Products Tabelle
Schema::table('products', function (Blueprint $table) {
$table->decimal('points', 10, 2)->unsigned()->nullable()->default(0)->change();
});
// 2. User Sales Volumes Tabelle
Schema::table('user_sales_volumes', function (Blueprint $table) {
$table->decimal('points', 10, 2)->nullable()->change();
$table->decimal('month_KP_points', 10, 2)->nullable()->change();
$table->decimal('month_TP_points', 10, 2)->nullable()->change();
$table->decimal('month_shop_points', 10, 2)->nullable()->change();
});
// 3. User Businesses Tabelle
Schema::table('user_businesses', function (Blueprint $table) {
$table->decimal('sales_volume_KP_points', 10, 2)->nullable()->change();
$table->decimal('sales_volume_TP_points', 10, 2)->nullable()->change();
$table->decimal('sales_volume_points_shop', 10, 2)->nullable()->change();
$table->decimal('sales_volume_points_KP_sum', 10, 2)->nullable()->change();
$table->decimal('sales_volume_points_TP_sum', 10, 2)->nullable()->change();
$table->decimal('payline_points', 10, 2)->nullable()->change();
$table->decimal('payline_points_qual_kp', 10, 2)->nullable()->change();
});
// 4. Shopping Orders Tabelle
Schema::table('shopping_orders', function (Blueprint $table) {
$table->decimal('points', 10, 2)->unsigned()->nullable()->change();
});
// 5. Shopping Order Items Tabelle
Schema::table('shopping_order_items', function (Blueprint $table) {
$table->decimal('points', 10, 2)->unsigned()->nullable()->default(0)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// 1. Products Tabelle zurück auf INT
Schema::table('products', function (Blueprint $table) {
$table->unsignedInteger('points')->nullable()->default(0)->change();
});
// 2. User Sales Volumes Tabelle zurück auf INT
Schema::table('user_sales_volumes', function (Blueprint $table) {
$table->integer('points')->nullable()->change();
$table->integer('month_KP_points')->nullable()->change();
$table->integer('month_TP_points')->nullable()->change();
$table->integer('month_shop_points')->nullable()->change();
});
// 3. User Businesses Tabelle zurück auf INT
Schema::table('user_businesses', function (Blueprint $table) {
$table->integer('sales_volume_KP_points')->nullable()->change();
$table->integer('sales_volume_TP_points')->nullable()->change();
$table->integer('sales_volume_points_shop')->nullable()->change();
$table->integer('sales_volume_points_KP_sum')->nullable()->change();
$table->integer('sales_volume_points_TP_sum')->nullable()->change();
$table->integer('payline_points')->nullable()->change();
$table->integer('payline_points_qual_kp')->nullable()->change();
});
// 4. Shopping Orders Tabelle zurück auf INT
Schema::table('shopping_orders', function (Blueprint $table) {
$table->unsignedInteger('points')->nullable()->change();
});
// 5. Shopping Order Items Tabelle zurück auf INT
Schema::table('shopping_order_items', function (Blueprint $table) {
$table->unsignedInteger('points')->nullable()->default(0)->change();
});
}
};

View file

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_bundles', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('product_id')->comment('Das Set/Kit (Parent)');
$table->unsignedInteger('bundle_product_id')->comment('Enthaltenes Produkt (Child)');
$table->unsignedInteger('quantity')->default(1);
$table->unsignedInteger('pos')->default(0)->comment('Sortierung');
$table->timestamps();
$table->foreign('product_id')
->references('id')
->on('products')
->onDelete('cascade');
$table->foreign('bundle_product_id')
->references('id')
->on('products')
->onDelete('cascade');
$table->unique(['product_id', 'bundle_product_id'], 'unique_bundle');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_bundles');
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Migration: DHL Postnummer Feld für Packstation-Lieferung
*
* Das Feld shipping_postnumber speichert die DHL Postnummer des Empfängers.
* Diese wird benötigt, wenn an eine Packstation oder Paketbox geliefert wird.
*
* Format: 6-10 stellige Nummer (z.B. "12345678")
* Wenn ausgefüllt: shipping_address enthält "Packstation XXX" oder "Paketbox XXX"
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('shopping_users', function (Blueprint $table) {
$table->string('shipping_postnumber', 20)->nullable()->after('shipping_phone');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('shopping_users', function (Blueprint $table) {
$table->dropColumn('shipping_postnumber');
});
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
$table->timestamp('tracking_email_sent_at')->nullable()->after('last_tracked_at');
$table->string('tracking_email_type', 20)->nullable()->after('tracking_email_sent_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
$table->dropColumn(['tracking_email_sent_at', 'tracking_email_type']);
});
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Migration: DHL Postnummer Feld für Packstation-Lieferung (User Accounts)
*
* Das Feld shipping_postnumber speichert die DHL Postnummer des User-Accounts.
* Diese wird benötigt, wenn an eine Packstation oder Paketbox geliefert wird.
*
* Format: 6-10 stellige Nummer (z.B. "12345678")
* Wenn ausgefüllt: shipping_address enthält "Packstation XXX" oder "Paketbox XXX"
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('user_accounts', function (Blueprint $table) {
$table->string('shipping_postnumber', 20)->nullable()->after('shipping_phone');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('user_accounts', function (Blueprint $table) {
$table->dropColumn('shipping_postnumber');
});
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('dashboard_news', function (Blueprint $table) {
$table->json('file_links')->nullable()->after('trans_content');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('dashboard_news', function (Blueprint $table) {
$table->dropColumn('file_links');
});
}
};

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
// Add email field for billing email
$table->string('email')->nullable()->after('company');
// Add postnumber field for DHL Postnummer (Packstation)
$table->string('postnumber', 20)->nullable()->after('email');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
$table->dropColumn(['email', 'postnumber']);
});
}
};

View file

@ -0,0 +1,93 @@
# DHL Sendungs-Stornierung - Troubleshooting
## Problem: "RF-UndefinedResource" Fehler
### Aktuelle Situation:
- **Config:** Sandbox-Modus aktiv (`DHL_SANDBOX=true`)
- **Base URL:** `https://api-sandbox.dhl.com`
- **Sendung:** ID 11, Nr. `0034043333301020021021115`
- **Status:** `created` (stornierbar)
- **Fehler:** "RF-UndefinedResource"
### Ursachen:
#### 1. **Sandbox-Einschränkungen**
Im DHL Sandbox-Modus:
- Sendungen sind nur für begrenzte Zeit verfügbar
- Test-Sendungen werden nach kurzer Zeit automatisch gelöscht
- Stornierung ist nur innerhalb weniger Stunden möglich
#### 2. **Zeitfenster überschritten**
- Sendung erstellt: `2026-01-23 13:08:07`
- Wenn mehr als 2-3 Stunden vergangen: Nicht mehr stornierbar
#### 3. **Sandbox vs. Production Mismatch**
- Sendung im Sandbox erstellt
- Versuch im Production-Modus zu stornieren (oder umgekehrt)
### Lösungen:
#### Option 1: Neue Test-Sendung erstellen
```bash
# Neue Sendung erstellen und SOFORT stornieren (innerhalb von Minuten)
# Im DHL Cockpit: Neue Sendung erstellen → Sofort Storno-Button klicken
```
#### Option 2: Production-Modus testen (nur mit echten Credentials!)
```env
# .env
DHL_SANDBOX=false
DHL_BASE_URL=https://api-eu.dhl.com
DHL_API_KEY=<production_key>
DHL_USERNAME=<production_user>
DHL_PASSWORD=<production_password>
```
#### Option 3: Status manuell setzen (nur für Entwicklung)
```bash
php artisan tinker
```
```php
$shipment = Acme\Dhl\Models\DhlShipment::find(11);
$shipment->update(['status' => 'canceled']);
```
### Empfehlung:
**Für Sandbox-Tests:**
1. Sendung erstellen
2. **SOFORT** (innerhalb von 1-2 Minuten) stornieren
3. Nicht länger warten
**Für Production:**
- Stornierung funktioniert zuverlässig innerhalb von 24 Stunden
- Sendungen bleiben länger in der API verfügbar
### Debug-Commands:
```bash
# Prüfe Sendung
php artisan tinker --execute="
\$s = Acme\Dhl\Models\DhlShipment::find(11);
echo 'Status: ' . \$s->status . PHP_EOL;
echo 'Created: ' . \$s->created_at . PHP_EOL;
echo 'Age: ' . \$s->created_at->diffForHumans() . PHP_EOL;
"
# Prüfe Config
php artisan tinker --execute="
\$c = (new \App\Http\Controllers\SettingController())->getDhlConfig();
echo 'Mode: ' . (\$c['base_url'] == 'https://api-sandbox.dhl.com' ? 'SANDBOX' : 'PRODUCTION') . PHP_EOL;
"
```
### Workaround für alte Sandbox-Sendungen:
Da alte Sandbox-Sendungen nicht mehr storniert werden können, setzen wir den Status manuell:
```php
// Alle alten "created" Sendungen als "failed" markieren
Acme\Dhl\Models\DhlShipment::where('status', 'created')
->where('created_at', '<', now()->subHours(6))
->update(['status' => 'failed']);
```

View file

@ -0,0 +1,362 @@
# DHL Return Label (Retourenlabel) - Dokumentation
## 📦 Übersicht
Die Return Label Funktionalität ermöglicht es, automatisch Retourenlabels für ausgehende DHL-Sendungen zu erstellen.
---
## ✨ Features
| Feature | Status | Beschreibung |
|---------|--------|--------------|
| **Button in allen Views** | ✅ | Überall wo DHL Sendungen angezeigt werden |
| **Sync/Async Modus** | ✅ | Basierend auf `DHL_USE_QUEUE` Config |
| **Adress-Tausch** | ✅ | Kunde wird Absender, Lager wird Empfänger |
| **Duplikat-Prüfung** | ✅ | Nur ein Retourenlabel pro Sendung |
| **Nur für Outbound** | ✅ | Nur für ausgehende Sendungen verfügbar |
| **Package Integration** | ✅ | Nutzt neues `acme-laravel-dhl` Package |
---
## 📍 Button Standorte
### **1. Bestelldetails**
```
Admin → Bestellungen → Bestellung öffnen
→ DHL Sendungen Block → Aktionen
[👁️] [📥] [🔄] [✉️] [🔙] [❌]
↑
Return Label Button
```
### **2. DHL Cockpit**
```
Admin → DHL → Cockpit
→ DataTable → Aktionen-Spalte
Gleiche Buttons wie in Bestelldetails
```
### **3. DHL Detail-Seite**
```
Admin → DHL → Sendung öffnen
→ Aktionsbereich oben
[🔙 Retourenlabel erstellen]
```
---
## 🎯 Button-Bedingungen
Der Button wird **nur** angezeigt wenn:
✅ `type` = 'outbound' (ausgehende Sendung)
✅ Keine Retoure existiert (`returns->count()` = 0)
✅ Admin-Bereich (nicht in User-Views)
```php
@if($shipment->type === 'outbound' && !$shipment->returns->count())
<button class="btn btn-outline-info dhl-create-return-btn">
<i class="fas fa-undo"></i>
</button>
@endif
```
---
## 🔄 Workflow
### **Synchroner Modus** (`DHL_USE_QUEUE=false`)
```
1. Admin klickt "Retourenlabel erstellen"
2. Bestätigung: "Möchten Sie ein Retourenlabel erstellen?"
3. → JA
4. Button: "⏳ Wird erstellt..."
5. API-Request direkt an DHL
6. ✅ Erfolg: "Retourenlabel wurde erfolgreich erstellt!"
7. Seite lädt neu
8. Neue Retoure ist sichtbar
```
### **Asynchroner Modus** (`DHL_USE_QUEUE=true`)
```
1. Admin klickt "Retourenlabel erstellen"
2. Bestätigung: "Möchten Sie ein Retourenlabel erstellen?"
3. → JA
4. Button: "⏳ Wird erstellt..."
5. Job wird in Queue gestellt
6. ✅ Erfolg: "Retourenlabel wird im Hintergrund erstellt"
7. Seite lädt neu
8. Worker verarbeitet Job
9. Retoure wird erstellt
```
---
## 🔧 Technische Details
### **Controller-Methode**
```php
// app/Http/Controllers/DhlShipmentController.php
public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
{
// Validierung
if ($shipment->type !== 'outbound') { ... }
if (existingReturn) { ... }
// Sync oder Async basierend auf Config
if ($useQueue) {
CreateReturnLabelJob::dispatch($shipment, $options);
} else {
$result = $this->createReturnLabelSync($shipment);
}
}
```
### **Job-Klasse**
```php
// app/Jobs/CreateReturnLabelJob.php
class CreateReturnLabelJob implements ShouldQueue
{
public function handle(): void
{
// DHL Client initialisieren
$dhlClient = new DhlClient(...);
$shippingService = new ShippingService($dhlClient);
// Return Label Daten vorbereiten (Adressen getauscht)
$returnData = $this->prepareReturnLabelData($dhlConfig);
// Label erstellen
$result = $shippingService->createLabel($returnData);
}
}
```
### **Adress-Tausch**
Bei Return Labels werden die Adressen **automatisch getauscht**:
| Feld | Ausgehende Sendung | Retoure |
|------|-------------------|---------|
| **Shipper (Absender)** | Unser Lager | **Kunde** |
| **Consignee (Empfänger)** | **Kunde** | Unser Lager |
```php
// Shipper: Kunde sendet zurück (aus original recipient)
'shipper' => [
'name' => 'Max Mustermann',
'street' => 'Hauptstraße',
'postalCode' => '12345',
'city' => 'Berlin',
...
],
// Consignee: Unser Lager empfängt (aus DHL settings)
'consignee' => [
'name' => 'mivita care gmbh',
'street' => 'Leinfeld',
'postalCode' => '87755',
'city' => 'Kirchhaslach',
...
],
```
---
## 🚨 Validierung & Fehlerbehandlung
### **Validierungs-Checks**
```php
// 1. Nur für ausgehende Sendungen
if ($shipment->type !== 'outbound') {
return 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.';
}
// 2. Keine doppelte Retoure
$existingReturn = DhlShipment::where('related_shipment_id', $shipment->id)
->where('type', 'return')
->first();
if ($existingReturn) {
return 'Für diese Sendung existiert bereits ein Retourenlabel.';
}
```
### **Fehlerbehandlung**
| Szenario | Verhalten |
|----------|-----------|
| API Timeout | Job wird wiederholt (3x) |
| Ungültige Adresse | Fehler + Log-Eintrag |
| Netzwerkfehler | Job wird wiederholt |
| 3 Fehlversuche | `failed()` Methode aufgerufen |
---
## 📊 Database Structure
### **Retoure-Beziehung**
```sql
-- Ausgehende Sendung
id: 123
type: 'outbound'
related_shipment_id: NULL
-- Zugehörige Retoure
id: 456
type: 'return'
related_shipment_id: 123 -- Verknüpfung zur Original-Sendung
```
### **Model-Beziehungen**
```php
// In DhlShipment Model
public function returns(): HasMany
{
return $this->hasMany(self::class, 'related_shipment_id');
}
public function relatedShipment(): BelongsTo
{
return $this->belongsTo(self::class, 'related_shipment_id');
}
```
---
## 🧪 Testen
### **Test 1: Retourenlabel erstellen (Sync)**
```bash
# 1. .env setzen
DHL_USE_QUEUE=false
# 2. Label erstellen für Bestellung
Admin → Bestellung #45078 → DHL Label erstellen
# 3. Return Label erstellen
→ Bestellung öffnen
→ DHL Sendungen Block
→ Button "🔙" klicken
→ Bestätigen
→ ✅ Retoure sollte sofort erscheinen
```
### **Test 2: Retourenlabel erstellen (Async)**
```bash
# 1. .env setzen
DHL_USE_QUEUE=true
# 2. Queue Worker starten
php artisan queue:work
# 3. Label erstellen
Admin → Bestellung #45078 → Button "🔙" klicken
# 4. Queue prüfen
→ Worker-Log anschauen
→ Retoure sollte nach wenigen Sekunden erscheinen
```
### **Test 3: Duplikat-Prüfung**
```bash
# 1. Retoure erstellen (wie Test 1)
# 2. Erneut Button "🔙" klicken
# → Fehler: "Für diese Sendung existiert bereits ein Retourenlabel."
```
---
## 📝 Logs
### **Erfolgreiche Erstellung**
```
[DHL Controller] Return label creation job dispatched
- original_shipment_id: 123
- shipment_number: 00340433333...
[DHL Queue] Return label created successfully
- original_shipment_id: 123
- return_shipment_number: 00340433444...
```
### **Fehler**
```
[DHL Queue] Return label creation failed
- original_shipment_id: 123
- error: "DHL API error: Invalid address"
- attempt: 1/3
```
---
## ⚙️ Konfiguration
### **Queue-Modus**
```env
# .env
DHL_USE_QUEUE=true # Async (empfohlen für Produktion)
DHL_USE_QUEUE=false # Sync (für Testing)
```
### **Queue-Name**
```php
// Normal Priority
$job->onQueue('dhl-returns');
// High Priority
$options['priority'] = 'high';
$job->onQueue('high-priority');
```
---
## 🔗 Routes
```php
// routes/domains/crm.php
Route::post('/shipment/{shipment}/return-label',
'DhlShipmentController@createReturnLabel')
->name('admin.dhl.create-return');
```
---
## 📋 Betroffene Dateien
| Datei | Änderung |
|-------|----------|
| `app/Http/Controllers/DhlShipmentController.php` | `createReturnLabel()` + `createReturnLabelSync()` |
| `app/Jobs/CreateReturnLabelJob.php` | Aktualisiert für neues Package |
| `resources/views/admin/sales/_detail_dhl_shipments.blade.php` | Button + JavaScript Handler |
| `resources/views/admin/dhl/show.blade.php` | Button aktiviert |
| `resources/views/admin/dhl/cockpit.blade.php` | Button funktioniert bereits |
---
## ✅ Checkliste
- [x] Button in allen DHL Views
- [x] JavaScript Handler implementiert
- [x] Controller-Methode mit Sync/Async
- [x] Job für Queue-Verarbeitung
- [x] Adress-Tausch korrekt
- [x] Validierung & Fehlerbehandlung
- [x] Duplikat-Prüfung
- [x] Logging implementiert
- [x] Dokumentation erstellt
---
## 🎉 Fertig!
Die Return Label Funktionalität ist vollständig implementiert und einsatzbereit!

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