Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)
Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert
Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c06740b9be
commit
d4e9d108ad
137 changed files with 7610 additions and 2955 deletions
|
|
@ -1,214 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DeleteAllKasSubdomains extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'subdomains:delete-all-kas
|
||||
{--domain= : Optional nur Subdomains dieser Hauptdomain löschen}
|
||||
{--limit= : Maximale Anzahl zu löschender Subdomains}
|
||||
{--list-file= : Optionaler Pfad für die temporäre JSON-Liste}
|
||||
{--force : Subdomains wirklich löschen; ohne diese Option läuft nur ein Dry-Run}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Löscht alle aktuell in KAS angelegten Subdomains anhand einer temporären KAS-Liste';
|
||||
|
||||
public function __construct(private KasController $kas)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$domain = $this->option('domain') !== null
|
||||
? trim((string) $this->option('domain'), '.')
|
||||
: null;
|
||||
$limit = $this->option('limit') !== null ? (int) $this->option('limit') : null;
|
||||
$isForce = (bool) $this->option('force');
|
||||
|
||||
if ($domain === '') {
|
||||
$this->error('Die Option --domain darf nicht leer sein.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($limit !== null && $limit < 1) {
|
||||
$this->error('Die Option --limit muss größer als 0 sein.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$candidates = $this->getKasSubdomains($domain);
|
||||
|
||||
if ($limit !== null) {
|
||||
$candidates = $candidates->take($limit)->values();
|
||||
}
|
||||
|
||||
$listFilePath = $this->writeTemporaryList($candidates, $domain, $isForce);
|
||||
$this->printPreview($candidates, $domain, $listFilePath, $isForce);
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
$this->info('Keine KAS-Subdomains gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $isForce) {
|
||||
$this->warn('Dry-Run: Es wurde nichts gelöscht. Mit --force werden die Subdomains aus der temporären Liste gelöscht.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
return $this->deleteSubdomains($candidates);
|
||||
} catch (Exception $e) {
|
||||
$this->error('Fehler beim Löschen der KAS-Subdomains: '.$e->getMessage());
|
||||
Log::error('KAS Komplett-Subdomain-Löschung fehlgeschlagen', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function getKasSubdomains(?string $domain): Collection
|
||||
{
|
||||
$this->info('Rufe Subdomains direkt von KAS ab...');
|
||||
|
||||
$subdomains = $this->kas->action('get_subdomains');
|
||||
|
||||
if (! is_iterable($subdomains)) {
|
||||
throw new Exception('KAS hat keine auswertbare Subdomain-Liste zurückgegeben: '.json_encode($subdomains));
|
||||
}
|
||||
|
||||
return collect($subdomains)
|
||||
->filter(function (array $subdomain) use ($domain): bool {
|
||||
$name = strtolower((string) ($subdomain['subdomain_name'] ?? ''));
|
||||
|
||||
if ($name === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $domain === null || str_ends_with($name, '.'.strtolower($domain));
|
||||
})
|
||||
->map(fn (array $subdomain): array => [
|
||||
'subdomain_name' => (string) $subdomain['subdomain_name'],
|
||||
'subdomain_path' => (string) ($subdomain['subdomain_path'] ?? ''),
|
||||
'php_version' => (string) ($subdomain['php_version'] ?? ''),
|
||||
'is_active' => (string) ($subdomain['is_active'] ?? ''),
|
||||
])
|
||||
->unique('subdomain_name')
|
||||
->sortBy('subdomain_name')
|
||||
->values();
|
||||
}
|
||||
|
||||
private function writeTemporaryList(Collection $candidates, ?string $domain, bool $isForce): string
|
||||
{
|
||||
$listFilePath = $this->resolveListFilePath();
|
||||
File::ensureDirectoryExists(dirname($listFilePath));
|
||||
|
||||
File::put($listFilePath, json_encode([
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'mode' => $isForce ? 'delete' : 'dry-run',
|
||||
'domain_filter' => $domain,
|
||||
'count' => $candidates->count(),
|
||||
'subdomains' => $candidates->values()->all(),
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $listFilePath;
|
||||
}
|
||||
|
||||
private function resolveListFilePath(): string
|
||||
{
|
||||
$listFilePath = (string) ($this->option('list-file') ?? '');
|
||||
|
||||
if ($listFilePath !== '') {
|
||||
return str_starts_with($listFilePath, '/') ? $listFilePath : base_path($listFilePath);
|
||||
}
|
||||
|
||||
return storage_path('app/kas-subdomain-delete-lists/kas-subdomains-'.now()->format('Ymd-His').'.json');
|
||||
}
|
||||
|
||||
private function printPreview(Collection $candidates, ?string $domain, string $listFilePath, bool $isForce): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info('Modus: '.($isForce ? 'Löschen' : 'Dry-Run'));
|
||||
$this->info('Domain-Filter: '.($domain ?: 'keiner'));
|
||||
$this->info('Temporäre Liste: '.$listFilePath);
|
||||
$this->info('Gefundene KAS-Subdomains: '.$candidates->count());
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Subdomain', 'Pfad', 'PHP', 'Aktiv'],
|
||||
$candidates->take(25)->map(fn (array $candidate): array => [
|
||||
$candidate['subdomain_name'],
|
||||
$candidate['subdomain_path'] ?: '-',
|
||||
$candidate['php_version'] ?: '-',
|
||||
$candidate['is_active'] ?: '-',
|
||||
])->all()
|
||||
);
|
||||
|
||||
if ($candidates->count() > 25) {
|
||||
$this->line('... weitere '.($candidates->count() - 25).' Subdomains ausgeblendet.');
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteSubdomains(Collection $candidates): int
|
||||
{
|
||||
$deleted = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$subdomainName = $candidate['subdomain_name'];
|
||||
$result = $this->kas->action('delete_subdomain', [
|
||||
'subdomain_name' => $subdomainName,
|
||||
]);
|
||||
|
||||
if ($this->isSuccessfulDeleteResult($result, $subdomainName)) {
|
||||
$deleted++;
|
||||
$this->info("Gelöscht: {$subdomainName}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$failed++;
|
||||
$this->error("Fehler beim Löschen von {$subdomainName}: ".json_encode($result));
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Gelöscht: {$deleted}");
|
||||
$this->info("Fehler: {$failed}");
|
||||
|
||||
return $failed === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function isSuccessfulDeleteResult(mixed $result, string $subdomainName): bool
|
||||
{
|
||||
return $result === true
|
||||
|| $result === 'TRUE'
|
||||
|| $result === $subdomainName;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Models\UserShop;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DeletePartnerShopSubdomains extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'subdomains:delete-partnershops
|
||||
{--domain=mivita.care : Hauptdomain der Partnershop-Subdomains}
|
||||
{--start=1 : UserShop-ID, ab der Shops berücksichtigt werden}
|
||||
{--limit= : Maximale Anzahl zu löschender Subdomains}
|
||||
{--active-only : Nur aktive/nicht gelöschte UserShops berücksichtigen}
|
||||
{--include-path-matches : Zusätzlich KAS-Subdomains mit dem Partnershop-Pfad löschen}
|
||||
{--path=/mein.mivita.care/public/ : Zielpfad der alten Partnershop-Subdomains}
|
||||
{--force : Subdomains wirklich löschen; ohne diese Option läuft nur ein Dry-Run}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Löscht alte KAS-Subdomains der Partnershops nach Umstellung auf Wildcard-Konfiguration';
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $protectedPrefixes = ['www.', 'api.', 'checkout.', 'preview.'];
|
||||
|
||||
public function __construct(private KasController $kas)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$domain = trim((string) $this->option('domain'), '.');
|
||||
$startId = max(1, (int) $this->option('start'));
|
||||
$limit = $this->option('limit') !== null ? (int) $this->option('limit') : null;
|
||||
$isForce = (bool) $this->option('force');
|
||||
|
||||
if ($domain === '') {
|
||||
$this->error('Die Option --domain darf nicht leer sein.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($limit !== null && $limit < 1) {
|
||||
$this->error('Die Option --limit muss größer als 0 sein.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$subdomains = $this->getKasSubdomains();
|
||||
$shopHosts = $this->getPartnerShopHosts($domain, $startId);
|
||||
$candidates = $this->getDeletionCandidates($subdomains, $shopHosts, $domain);
|
||||
|
||||
if ($limit !== null) {
|
||||
$candidates = $candidates->take($limit)->values();
|
||||
}
|
||||
|
||||
$this->printPreview($candidates, $isForce);
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
$this->info('Keine löschbaren Partnershop-Subdomains gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $isForce) {
|
||||
$this->warn('Dry-Run: Es wurde nichts gelöscht. Mit --force werden die gefundenen Subdomains gelöscht.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
return $this->deleteSubdomains($candidates);
|
||||
} catch (Exception $e) {
|
||||
$this->error('Fehler beim Löschen der Partnershop-Subdomains: '.$e->getMessage());
|
||||
Log::error('KAS Partnershop-Subdomain-Löschung fehlgeschlagen', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function getKasSubdomains(): Collection
|
||||
{
|
||||
$this->info('Rufe Subdomains von KAS ab...');
|
||||
|
||||
$subdomains = $this->kas->action('get_subdomains');
|
||||
|
||||
if (! is_iterable($subdomains)) {
|
||||
throw new Exception('KAS hat keine auswertbare Subdomain-Liste zurückgegeben: '.json_encode($subdomains));
|
||||
}
|
||||
|
||||
return collect($subdomains);
|
||||
}
|
||||
|
||||
private function getPartnerShopHosts(string $domain, int $startId): Collection
|
||||
{
|
||||
$query = UserShop::query()
|
||||
->where('id', '>=', $startId)
|
||||
->orderBy('id');
|
||||
|
||||
if ($this->option('active-only')) {
|
||||
$query->where('active', true);
|
||||
} else {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
return $query
|
||||
->pluck('slug')
|
||||
->filter()
|
||||
->map(fn (string $slug): string => strtolower($slug.'.'.$domain))
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
|
||||
private function getDeletionCandidates(Collection $subdomains, Collection $shopHosts, string $domain): Collection
|
||||
{
|
||||
$shopHostLookup = array_flip($shopHosts->all());
|
||||
$includePathMatches = (bool) $this->option('include-path-matches');
|
||||
$partnerShopPath = (string) $this->option('path');
|
||||
|
||||
return $subdomains
|
||||
->filter(function (array $subdomain) use ($domain, $includePathMatches, $partnerShopPath, $shopHostLookup): bool {
|
||||
$name = strtolower((string) ($subdomain['subdomain_name'] ?? ''));
|
||||
|
||||
if ($name === '' || ! $this->belongsToDomain($name, $domain) || $this->isProtectedSubdomain($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($shopHostLookup[$name])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $includePathMatches
|
||||
&& trim((string) ($subdomain['subdomain_path'] ?? '')) === $partnerShopPath;
|
||||
})
|
||||
->map(fn (array $subdomain): array => [
|
||||
'subdomain_name' => (string) $subdomain['subdomain_name'],
|
||||
'subdomain_path' => (string) ($subdomain['subdomain_path'] ?? ''),
|
||||
'php_version' => (string) ($subdomain['php_version'] ?? ''),
|
||||
])
|
||||
->unique('subdomain_name')
|
||||
->sortBy('subdomain_name')
|
||||
->values();
|
||||
}
|
||||
|
||||
private function belongsToDomain(string $subdomainName, string $domain): bool
|
||||
{
|
||||
return str_ends_with($subdomainName, '.'.strtolower($domain));
|
||||
}
|
||||
|
||||
private function isProtectedSubdomain(string $subdomainName): bool
|
||||
{
|
||||
foreach ($this->protectedPrefixes as $prefix) {
|
||||
if (str_starts_with($subdomainName, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function printPreview(Collection $candidates, bool $isForce): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info('Modus: '.($isForce ? 'Löschen' : 'Dry-Run'));
|
||||
$this->info('Gefundene Lösch-Kandidaten: '.$candidates->count());
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Subdomain', 'Pfad', 'PHP'],
|
||||
$candidates->take(25)->map(fn (array $candidate): array => [
|
||||
$candidate['subdomain_name'],
|
||||
$candidate['subdomain_path'] ?: '-',
|
||||
$candidate['php_version'] ?: '-',
|
||||
])->all()
|
||||
);
|
||||
|
||||
if ($candidates->count() > 25) {
|
||||
$this->line('... weitere '.($candidates->count() - 25).' Subdomains ausgeblendet.');
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteSubdomains(Collection $candidates): int
|
||||
{
|
||||
$deleted = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$subdomainName = $candidate['subdomain_name'];
|
||||
$result = $this->kas->action('delete_subdomain', [
|
||||
'subdomain_name' => $subdomainName,
|
||||
]);
|
||||
|
||||
if ($this->isSuccessfulDeleteResult($result, $subdomainName)) {
|
||||
$deleted++;
|
||||
$this->info("Gelöscht: {$subdomainName}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$failed++;
|
||||
$this->error("Fehler beim Löschen von {$subdomainName}: ".json_encode($result));
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Gelöscht: {$deleted}");
|
||||
$this->info("Fehler: {$failed}");
|
||||
|
||||
return $failed === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function isSuccessfulDeleteResult(mixed $result, string $subdomainName): bool
|
||||
{
|
||||
return $result === true
|
||||
|| $result === 'TRUE'
|
||||
|| $result === $subdomainName;
|
||||
}
|
||||
}
|
||||
72
app/Console/Commands/GdprDeleteUser.php
Normal file
72
app/Console/Commands/GdprDeleteUser.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\GdprDeletionService;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GdprDeleteUser extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'gdpr:delete
|
||||
{id : User-ID (Berater) bzw. mit --customer die ShoppingUser-ID (Endkunde)}
|
||||
{--customer : Endkunde (shopping_users) statt Berater (users) löschen}
|
||||
{--force : Sicherheitsabfrage überspringen}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'DSGVO-Löschung (Art. 17): Anonymisiert alle personenbezogenen Daten eines Beraters oder Endkunden unwiderruflich. Aufbewahrungspflichtige Daten (Rechnungen, Gutschriften, Provisionen) bleiben erhalten.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(GdprDeletionService $gdprDeletionService): int
|
||||
{
|
||||
$id = (int) $this->argument('id');
|
||||
$isCustomer = (bool) $this->option('customer');
|
||||
|
||||
if ($isCustomer) {
|
||||
$model = ShoppingUser::withTrashed()->find($id);
|
||||
$label = $model ? 'Endkunde #'.$model->id.' ('.$model->billing_email.')' : null;
|
||||
} else {
|
||||
$model = User::withTrashed()->find($id);
|
||||
$label = $model ? 'Berater #'.$model->id.' ('.$model->email.')' : null;
|
||||
}
|
||||
|
||||
if (! $model) {
|
||||
$this->error('ERROR: Datensatz nicht gefunden, id: '.$id);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm('DSGVO-Löschung für '.$label.' UNWIDERRUFLICH ausführen?')) {
|
||||
$this->info('Abgebrochen.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$summary = $isCustomer
|
||||
? $gdprDeletionService->deleteShoppingUser($model)
|
||||
: $gdprDeletionService->deleteUser($model);
|
||||
|
||||
$this->info('SUCCESS: DSGVO-Löschung ausgeführt: '.json_encode($summary));
|
||||
|
||||
return 0;
|
||||
} catch (GdprDeletionException $e) {
|
||||
$this->error('ERROR: '.$e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,664 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Http\Controllers\Api\KasSLLController;
|
||||
use App\Models\UserShop;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SubDomains extends Command
|
||||
{
|
||||
/**
|
||||
* Die Signatur des Konsolenbefehls.
|
||||
*
|
||||
* Aufruf: php artisan subdomains:action --force --start=4 --debug
|
||||
* /usr/bin/php82 artisan subdomains:action --force --start=4 --create-missing --debug
|
||||
* /usr/bin/php82 artisan subdomains:action --force --debug
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'subdomains:action {user_id?} {--force} {--start=1} {--create-missing} {--debug}';
|
||||
|
||||
/**
|
||||
* Die Beschreibung des Konsolenbefehls.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Passt Parameter für die User-Subdomains an (PHP-Version, SSL)';
|
||||
|
||||
/**
|
||||
* Zeitstempel für die Ausführungszeit-Messung
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
private $timeStart;
|
||||
|
||||
/**
|
||||
* Standard-Domain für alle Subdomains
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $domain = 'mivita.care';
|
||||
|
||||
/**
|
||||
* Zu überspringende Subdomain-Präfixe
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $skipPrefixes = ['www.', 'api.', 'checkout.', 'preview.'];
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Befehlsinstanz.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den Konsolenbefehl aus.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->timeStart = microtime(true);
|
||||
|
||||
$userId = $this->argument('user_id');
|
||||
$force = $this->option('force');
|
||||
$startId = $this->option('start');
|
||||
$createMissing = $this->option('create-missing');
|
||||
$debug = $this->option('debug');
|
||||
|
||||
if ($debug) {
|
||||
$this->warn("DEBUG-MODUS (DRY-RUN): Es werden keine tatsächlichen Änderungen vorgenommen!");
|
||||
}
|
||||
|
||||
$this->info("Starte Subdomain-Verwaltung" . ($force ? " (erzwungener Modus)" : ""));
|
||||
|
||||
try {
|
||||
if ($userId) {
|
||||
$this->info("Verarbeite einzelnen Benutzer mit ID: {$userId}");
|
||||
$result = $this->syncSingleUser($userId, $force, $createMissing, $debug);
|
||||
|
||||
if ($result) {
|
||||
$this->info("Benutzer {$userId} erfolgreich synchronisiert" . ($debug ? " (simuliert)" : ""));
|
||||
} else {
|
||||
$this->warn("Benutzer {$userId} konnte nicht vollständig synchronisiert werden" . ($debug ? " (simuliert)" : ""));
|
||||
}
|
||||
} else {
|
||||
$this->info("Verarbeite alle Benutzer ab ID: {$startId}");
|
||||
$result = $this->syncAllUsers($force, $startId, $createMissing, $debug);
|
||||
|
||||
// Zusammenfassung der Ergebnisse
|
||||
$this->displaySummary($result, $debug);
|
||||
}
|
||||
|
||||
$this->logExecutionTime("Subdomain-Verwaltung abgeschlossen" . ($debug ? " (DEBUG-MODUS)" : ""));
|
||||
return 0;
|
||||
} catch (Exception $e) {
|
||||
$this->error("Ein Fehler ist aufgetreten: " . $e->getMessage());
|
||||
Log::error("Shopping User Sync Fehler: ", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'user_id' => $userId,
|
||||
'force' => $force,
|
||||
'start_id' => $startId,
|
||||
'debug' => $debug
|
||||
]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Zeigt eine Zusammenfassung der Synchronisationsergebnisse an
|
||||
*
|
||||
* @param array $result Ergebnisdaten der Synchronisation
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return void
|
||||
*/
|
||||
private function displaySummary($result, $debug = false)
|
||||
{
|
||||
$this->line("");
|
||||
$this->info("=== Zusammenfassung " . ($debug ? "(DEBUG-MODUS)" : "") . " ===");
|
||||
$this->info("Verarbeitete Shops: " . count($result['shops']));
|
||||
$this->info("Aktualisierte PHP-Versionen: " . $result['updatedCount'] . ($debug ? " (simuliert)" : ""));
|
||||
$this->info("Aktivierte SSL-Zertifikate: " . $result['sslEnabledCount'] . ($debug ? " (simuliert)" : ""));
|
||||
$this->info("Aktualisierte SSL-Konfigurationen: " . $result['sslConfiguredCount'] . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if (!empty($result['createdSubdomains'])) {
|
||||
$this->info("Neu erstellte Subdomains: " . count($result['createdSubdomains']) . ($debug ? " (simuliert)" : ""));
|
||||
}
|
||||
|
||||
if (!empty($result['missingSubdomains'])) {
|
||||
$this->warn("Fehlende Subdomains: " . count($result['missingSubdomains']));
|
||||
}
|
||||
|
||||
if (!empty($result['unusedSubdomains'])) {
|
||||
$this->warn("Ungenutzte Subdomains: " . count($result['unusedSubdomains']));
|
||||
}
|
||||
|
||||
if (!empty($result['doubleDomains'])) {
|
||||
$this->warn("Benutzer mit mehreren Shops: " . count($result['doubleDomains']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert einen einzelnen Benutzer und seine Shops
|
||||
*
|
||||
* @param int $userId Benutzer-ID
|
||||
* @param bool $force Erzwingt die Aktualisierung aller Subdomains
|
||||
* @param bool $createMissing Erstellt fehlende Subdomains
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function syncSingleUser($userId, $force = false, $createMissing = false, $debug = false)
|
||||
{
|
||||
$this->info("Synchronisiere Benutzer mit ID: {$userId}");
|
||||
|
||||
// Benutzer-Shops abrufen
|
||||
$userShops = UserShop::where('user_id', $userId)->get();
|
||||
|
||||
if ($userShops->isEmpty()) {
|
||||
$this->warn("Keine Shops für Benutzer {$userId} gefunden");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("Gefundene Shops für Benutzer {$userId}: " . $userShops->count());
|
||||
|
||||
// Subdomains abrufen und filtern
|
||||
$subdomains = $this->getFilteredSubdomains();
|
||||
$success = true;
|
||||
|
||||
// Benutzer-Shops durchlaufen und mit Subdomains abgleichen
|
||||
foreach ($userShops as $userShop) {
|
||||
$fullDomainName = $userShop->slug . '.' . $this->domain;
|
||||
$this->info("Verarbeite Shop: {$fullDomainName}");
|
||||
|
||||
// Prüfen, ob Subdomain existiert
|
||||
if (array_key_exists($fullDomainName, $subdomains)) {
|
||||
$success = $this->processExistingSubdomain($userShop, $subdomains[$fullDomainName], $force, $debug) && $success;
|
||||
} else {
|
||||
// Subdomain fehlt
|
||||
$this->warn("Shop {$userShop->slug}: Keine Subdomain gefunden");
|
||||
|
||||
// Optional: Neue Subdomain erstellen
|
||||
if ($createMissing) {
|
||||
$this->info("Erstelle fehlende Subdomain für Shop {$userShop->slug}" . ($debug ? " (simuliert)" : ""));
|
||||
$success = $this->createSubdomain($userShop->slug, $debug) && $success;
|
||||
} else {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine existierende Subdomain und aktualisiert sie bei Bedarf
|
||||
*
|
||||
* @param UserShop $userShop Shop-Objekt
|
||||
* @param array $subdomainInfo Subdomain-Informationen
|
||||
* @param bool $force Erzwingt die Aktualisierung
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function processExistingSubdomain($userShop, $subdomainInfo, $force, $debug = false)
|
||||
{
|
||||
$success = true;
|
||||
$hasSSL = ($subdomainInfo['ssl_certificate_sni'] === 'Y' ||
|
||||
$subdomainInfo['ssl_proxy'] === 'Y');
|
||||
$sslActive = ($subdomainInfo['ssl_certificate_sni_is_active'] ?? 'N') === 'Y';
|
||||
$phpVersion = $subdomainInfo['php_version'];
|
||||
|
||||
$this->info("Shop {$userShop->slug}: PHP-Version: {$phpVersion}, SSL: " .
|
||||
($hasSSL ? "Aktiviert" : "Nicht aktiviert") .
|
||||
($hasSSL ? ", SSL aktiv: " . ($sslActive ? "Ja" : "Nein") : ""));
|
||||
|
||||
// Prüfen, ob PHP-Version aktualisiert werden muss
|
||||
$requiredPhpVersion = config('app.php_version');
|
||||
if ($force || $phpVersion !== $requiredPhpVersion) {
|
||||
$this->info("Shop {$userShop->slug}: PHP-Version aktualisieren von {$phpVersion} auf {$requiredPhpVersion}" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if (!$debug && !$this->updateSubdomainParams($userShop->slug, $requiredPhpVersion)) {
|
||||
$this->error("PHP-Version für {$userShop->slug}.{$this->domain} konnte nicht aktualisiert werden");
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen, ob SSL aktiviert werden muss
|
||||
if ($force || !$hasSSL) {
|
||||
$this->info("Shop {$userShop->slug}: SSL aktivieren" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if (!$debug && !$this->enableSSL($userShop->slug)) {
|
||||
$this->error("SSL für {$userShop->slug}.{$this->domain} konnte nicht aktiviert werden");
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
// Prüfen, ob SSL-Konfiguration aktualisiert werden muss
|
||||
else if ($force || ($hasSSL && !$sslActive)) {
|
||||
$this->info("Shop {$userShop->slug}: SSL-Konfiguration aktualisieren" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if (!$debug && !$this->updateSSL($userShop->slug . '.' . $this->domain)) {
|
||||
$this->error("SSL-Konfiguration für {$userShop->slug}.{$this->domain} konnte nicht aktualisiert werden");
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert alle Benutzer-Shops
|
||||
*
|
||||
* @param bool $force Erzwingt die Aktualisierung aller Subdomains
|
||||
* @param int $startId Beginnt die Synchronisation ab dieser ID
|
||||
* @param bool $createMissing Erstellt fehlende Subdomains
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return array Ergebnisdaten der Synchronisation
|
||||
*/
|
||||
private function syncAllUsers($force, $startId, $createMissing = false, $debug = false)
|
||||
{
|
||||
$this->info("Starte Synchronisation aller Benutzer-Shops ab ID: {$startId}");
|
||||
|
||||
// Benutzer-Shops abrufen
|
||||
$userShopsQuery = UserShop::query();
|
||||
|
||||
if ($startId > 1) {
|
||||
$userShopsQuery->where('id', '>=', $startId);
|
||||
}
|
||||
|
||||
$userShops = $userShopsQuery->limit(1000)->get();
|
||||
$this->info("Gefundene Benutzer-Shops: " . $userShops->count());
|
||||
|
||||
// Subdomains abrufen und filtern
|
||||
$subdomains = $this->getFilteredSubdomains();
|
||||
$this->info("Gefilterte Subdomains: " . count($subdomains));
|
||||
|
||||
// Ergebnis-Arrays initialisieren
|
||||
$doubleDomains = [];
|
||||
$missingSubdomains = [];
|
||||
$outdatedPhpVersions = [];
|
||||
$missingSSL = [];
|
||||
$sslConfigurationNeeded = [];
|
||||
$createdSubdomains = [];
|
||||
$updatedCount = 0;
|
||||
$sslEnabledCount = 0;
|
||||
$sslConfiguredCount = 0;
|
||||
|
||||
// Benutzer-Shops durchlaufen und mit Subdomains abgleichen
|
||||
foreach ($userShops as $userShop) {
|
||||
$fullDomainName = $userShop->slug . '.' . $this->domain;
|
||||
|
||||
// Status der Subdomain setzen
|
||||
$userShop->hasSubdomain = false;
|
||||
$userShop->hasSSL = false;
|
||||
$userShop->sslActive = false;
|
||||
$userShop->PHPversion = "";
|
||||
|
||||
// Prüfen, ob Subdomain existiert
|
||||
if (array_key_exists($fullDomainName, $subdomains)) {
|
||||
$userShop->hasSubdomain = true;
|
||||
$userShop->hasSSL = ($subdomains[$fullDomainName]['ssl_certificate_sni'] === 'Y' ||
|
||||
$subdomains[$fullDomainName]['ssl_proxy'] === 'Y');
|
||||
$userShop->sslActive = ($subdomains[$fullDomainName]['ssl_certificate_sni_is_active'] ?? 'N') === 'Y';
|
||||
$userShop->PHPversion = $subdomains[$fullDomainName]['php_version'];
|
||||
|
||||
// Prüfen, ob PHP-Version aktualisiert werden muss
|
||||
$requiredPhpVersion = config('app.php_version');
|
||||
if ($force || $userShop->PHPversion !== $requiredPhpVersion) {
|
||||
$this->info("Shop {$userShop->slug}: PHP-Version aktualisieren von {$userShop->PHPversion} auf {$requiredPhpVersion}" . ($debug ? " (simuliert)" : ""));
|
||||
$outdatedPhpVersions[] = $userShop->slug;
|
||||
|
||||
if (!$debug && $this->updateSubdomainParams($userShop->slug, $requiredPhpVersion)) {
|
||||
$updatedCount++;
|
||||
} else if ($debug) {
|
||||
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
|
||||
$updatedCount++;
|
||||
}
|
||||
}else{
|
||||
$this->info("Shop {$userShop->slug}: PHP-Version ist aktuell: {$userShop->PHPversion}");
|
||||
}
|
||||
|
||||
// Prüfen, ob SSL aktiviert werden muss
|
||||
/* if ($force || !$userShop->hasSSL) {
|
||||
$this->info("Shop {$userShop->slug}: SSL aktivieren" . ($debug ? " (simuliert)" : ""));
|
||||
$missingSSL[] = $userShop->slug;
|
||||
|
||||
if (!$debug && $this->enableSSL($userShop->slug)) {
|
||||
$sslEnabledCount++;
|
||||
} else if ($debug) {
|
||||
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
|
||||
$sslEnabledCount++;
|
||||
}
|
||||
}
|
||||
// Prüfen, ob SSL-Konfiguration aktualisiert werden muss
|
||||
else if ($force || ($userShop->hasSSL && !$userShop->sslActive)) {
|
||||
$this->info("Shop {$userShop->slug}: SSL-Konfiguration aktualisieren" . ($debug ? " (simuliert)" : ""));
|
||||
$sslConfigurationNeeded[] = $userShop->slug;
|
||||
|
||||
if (!$debug && $this->updateSSL($fullDomainName)) {
|
||||
$sslConfiguredCount++;
|
||||
} else if ($debug) {
|
||||
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
|
||||
$sslConfiguredCount++;
|
||||
}
|
||||
}*/
|
||||
|
||||
// Subdomain aus der Liste entfernen, um später ungenutzte zu identifizieren
|
||||
unset($subdomains[$fullDomainName]);
|
||||
} else {
|
||||
// Subdomain fehlt
|
||||
$missingSubdomains[] = $userShop->slug;
|
||||
$this->warn("Shop {$userShop->slug}: Keine Subdomain gefunden");
|
||||
|
||||
// Optional: Neue Subdomain erstellen
|
||||
if ($createMissing) {
|
||||
$this->info("Erstelle fehlende Subdomain für Shop {$userShop->slug}" . ($debug ? " (simuliert)" : ""));
|
||||
if (!$debug && $this->createSubdomain($userShop->slug)) {
|
||||
$createdSubdomains[] = $userShop->slug;
|
||||
} else if ($debug) {
|
||||
// Im Debug-Modus zählen wir trotzdem, als ob es erfolgreich wäre
|
||||
$createdSubdomains[] = $userShop->slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doppelte Domains pro Benutzer erfassen
|
||||
$doubleDomains[$userShop->user_id][$userShop->id] = $fullDomainName;
|
||||
}
|
||||
|
||||
// Bereinigen der doppelten Domains (nur Benutzer mit mehreren Shops behalten)
|
||||
foreach ($doubleDomains as $userId => $domains) {
|
||||
if (count($domains) === 1) {
|
||||
unset($doubleDomains[$userId]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logExecutionTime("Synchronisation abgeschlossen" . ($debug ? " (DEBUG-MODUS)" : ""));
|
||||
|
||||
// Ergebnisdaten zurückgeben
|
||||
return [
|
||||
'shops' => $userShops,
|
||||
'unusedSubdomains' => $subdomains,
|
||||
'doubleDomains' => $doubleDomains,
|
||||
'missingSubdomains' => $missingSubdomains,
|
||||
'outdatedPhpVersions' => $outdatedPhpVersions,
|
||||
'missingSSL' => $missingSSL,
|
||||
'sslConfigurationNeeded' => $sslConfigurationNeeded,
|
||||
'createdSubdomains' => $createdSubdomains,
|
||||
'updatedCount' => $updatedCount,
|
||||
'sslEnabledCount' => $sslEnabledCount,
|
||||
'sslConfiguredCount' => $sslConfiguredCount
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft alle Subdomains ab und filtert sie
|
||||
*
|
||||
* @return array Gefilterte Subdomains
|
||||
*/
|
||||
private function getFilteredSubdomains()
|
||||
{
|
||||
$kas = new KasController();
|
||||
$subdomains = [];
|
||||
|
||||
// Alle Subdomains abrufen
|
||||
$this->info("Rufe Subdomains von KAS ab...");
|
||||
$getSubdomains = $kas->action('get_subdomains');
|
||||
|
||||
// Subdomains filtern und in ein leicht zugängliches Array umwandeln
|
||||
foreach ($getSubdomains as $subdomain) {
|
||||
if (!isset($subdomain['subdomain_name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Spezielle Subdomains überspringen
|
||||
$skip = false;
|
||||
foreach ($this->skipPrefixes as $prefix) {
|
||||
if (strpos($subdomain['subdomain_name'], $prefix) !== false) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Subdomain-Informationen speichern
|
||||
$subdomains[$subdomain['subdomain_name']] = [
|
||||
'ssl_certificate_sni' => $subdomain['ssl_certificate_sni'] ?? 'N',
|
||||
'php_version' => $subdomain['php_version'] ?? '',
|
||||
'ssl_proxy' => $subdomain['ssl_proxy'] ?? 'N',
|
||||
'ssl_certificate_sni_is_active' => $subdomain['ssl_certificate_sni_is_active'] ?? 'N',
|
||||
];
|
||||
}
|
||||
|
||||
return $subdomains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ändert Parameter einer Subdomain, insbesondere die PHP-Version
|
||||
*
|
||||
* @param string $subdomain Name der Subdomain ohne Domain
|
||||
* @param string $phpVersion Neue PHP-Version (z.B. '8.2')
|
||||
* @param array $additionalParams Weitere zu ändernde Parameter
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function updateSubdomainParams($subdomain, $phpVersion, $additionalParams = [], $debug = false)
|
||||
{
|
||||
$this->info("Aktualisiere Parameter für: {$subdomain}.{$this->domain}" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if ($debug) {
|
||||
$this->line(" - PHP-Version: {$phpVersion}");
|
||||
if (!empty($additionalParams)) {
|
||||
$this->line(" - Zusätzliche Parameter: " . json_encode($additionalParams));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$kas = new KasController();
|
||||
|
||||
// Standardparameter
|
||||
$params = [
|
||||
'subdomain_name' => $subdomain . '.' . $this->domain, // Vollständigen Domainnamen verwenden
|
||||
'php_version' => $phpVersion
|
||||
];
|
||||
|
||||
// Oder alternativ, falls die API die Subdomain und Domain getrennt erwartet:
|
||||
// $params = [
|
||||
// 'subdomain_name' => $subdomain,
|
||||
// 'domain_name' => $this->domain,
|
||||
// 'php_version' => $phpVersion
|
||||
// ];
|
||||
|
||||
// Zusätzliche Parameter hinzufügen
|
||||
$params = array_merge($params, $additionalParams);
|
||||
|
||||
// Subdomain aktualisieren
|
||||
$result = $kas->action('update_subdomain', $params);
|
||||
$this->info("Parameter: ".json_encode($params));
|
||||
|
||||
if ($result) {
|
||||
$this->info("Parameter für {$subdomain}.{$this->domain} erfolgreich aktualisiert " . json_encode($result));
|
||||
return true;
|
||||
} else {
|
||||
$this->error("Fehler beim Aktualisieren der Parameter für {$subdomain}.{$this->domain}: " . json_encode($result));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->error("Fehler beim Aktualisieren der Parameter für {$subdomain}.{$this->domain}: " . $e->getMessage());
|
||||
Log::error("Subdomain Parameter Update Fehler", [
|
||||
'subdomain' => $subdomain,
|
||||
'domain' => $this->domain,
|
||||
'php_version' => $phpVersion,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die SSL-Konfiguration einer Subdomain mit erweiterten Parametern
|
||||
*
|
||||
* @param string $subdomainName Vollständiger Domainname (z.B. 'shop.mivita.care')
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function updateSSL($subdomainName, $debug = false)
|
||||
{
|
||||
$this->info("Aktualisiere SSL-Konfiguration für: {$subdomainName}" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if ($debug) {
|
||||
$this->line(" - SSL-Parameter werden aktualisiert");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$kas = new KasController();
|
||||
$ssl = KasSLLController::getApiSSLParameter();
|
||||
|
||||
$params = array_merge(['hostname' => $subdomainName], $ssl);
|
||||
$result = $kas->action('update_ssl', $params);
|
||||
|
||||
if ($result === "TRUE" || $result === true) {
|
||||
$this->info("SSL-Konfiguration für {$subdomainName} erfolgreich aktualisiert");
|
||||
return true;
|
||||
} else {
|
||||
$this->warn("SSL-Konfiguration für {$subdomainName} nicht vollständig aktualisiert: " . json_encode($result));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->error("Fehler bei der SSL-Konfiguration für {$subdomainName}: " . $e->getMessage());
|
||||
Log::error("SSL Update Fehler", [
|
||||
'subdomain' => $subdomainName,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert SSL für eine Subdomain mit vollständiger Konfiguration
|
||||
*
|
||||
* @param string $subdomain Name der Subdomain ohne Domain
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function enableSSL($subdomain, $debug = false)
|
||||
{
|
||||
$fullDomainName = $subdomain . '.' . $this->domain;
|
||||
$this->info("Aktiviere SSL für: {$fullDomainName}" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if ($debug) {
|
||||
$this->line(" - SSL-Proxy wird aktiviert");
|
||||
$this->line(" - HTTP zu HTTPS Weiterleitung wird eingerichtet");
|
||||
$this->line(" - SSL-Konfiguration wird aktualisiert");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Schritt 1: Subdomain-Parameter aktualisieren (ssl_proxy aktivieren)
|
||||
$subdomainResult = $this->updateSubdomainParams($subdomain, config('app.php_version'), [
|
||||
'ssl_proxy' => 'Y',
|
||||
'redirect_status' => 301 // Weiterleitung von HTTP auf HTTPS
|
||||
]);
|
||||
|
||||
if (!$subdomainResult) {
|
||||
$this->error("SSL-Aktivierung für {$fullDomainName} fehlgeschlagen: Subdomain-Parameter konnten nicht aktualisiert werden");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Schritt 2: SSL-Konfiguration aktualisieren
|
||||
$sslResult = $this->updateSSL($fullDomainName);
|
||||
|
||||
if (!$sslResult) {
|
||||
$this->warn("SSL-Aktivierung für {$fullDomainName} teilweise erfolgreich: SSL-Konfiguration konnte nicht aktualisiert werden");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("SSL für {$fullDomainName} vollständig aktiviert");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Subdomain für einen Shop
|
||||
*
|
||||
* @param string $slug Shop-Slug
|
||||
* @param bool $debug Debug-Modus (Dry-Run)
|
||||
* @return bool Erfolg der Operation
|
||||
*/
|
||||
private function createSubdomain($slug, $debug = false)
|
||||
{
|
||||
$fullDomainName = $slug . '.' . $this->domain;
|
||||
$this->info("Erstelle neue Subdomain: {$fullDomainName}" . ($debug ? " (simuliert)" : ""));
|
||||
|
||||
if ($debug) {
|
||||
$this->line(" - Pfad: /mein.mivita.care/public/");
|
||||
$this->line(" - PHP-Version: " . config('app.php_version'));
|
||||
$this->line(" - SSL wird direkt aktiviert");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$kas = new KasController();
|
||||
|
||||
// Parameter für die neue Subdomain
|
||||
$params = [
|
||||
'subdomain_name' => $slug,
|
||||
'domain_name' => $this->domain,
|
||||
'subdomain_path' => '/mein.mivita.care/public/',
|
||||
'php_version' => config('app.php_version'),
|
||||
];
|
||||
|
||||
// Subdomain erstellen
|
||||
$result = $kas->action('add_subdomain', $params);
|
||||
|
||||
if ($result === $fullDomainName) {
|
||||
$this->info("Subdomain {$fullDomainName} erfolgreich erstellt");
|
||||
|
||||
// SSL direkt aktivieren
|
||||
$this->enableSSL($slug);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$this->error("Fehler beim Erstellen der Subdomain {$fullDomainName}: " . json_encode($result));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->error("Fehler beim Erstellen der Subdomain {$fullDomainName}: " . $e->getMessage());
|
||||
Log::error("Subdomain Erstellung Fehler", [
|
||||
'slug' => $slug,
|
||||
'domain' => $this->domain,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Protokolliert die Ausführungszeit einer Operation
|
||||
*
|
||||
* @param string $message Nachricht für die Protokollierung
|
||||
* @return void
|
||||
*/
|
||||
private function logExecutionTime($message)
|
||||
{
|
||||
$diff = microtime(true) - $this->timeStart;
|
||||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
$this->info($message. ' | Time: '.$sec. 'sec :' . round($micro * 1000, 4) . " ms");
|
||||
}
|
||||
}
|
||||
11
app/Exceptions/GdprDeletionException.php
Normal file
11
app/Exceptions/GdprDeletionException.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Wird geworfen, wenn eine DSGVO-Löschung nicht ausgeführt werden kann
|
||||
* (z. B. Admin-Account, aktives Abo, Datensatz gehört zu einem Berater).
|
||||
*/
|
||||
class GdprDeletionException extends Exception {}
|
||||
|
|
@ -117,6 +117,7 @@ class AboController extends Controller
|
|||
if (isset($data['action'])) {
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
$user_abo = UserAbo::findOrFail($data['id']);
|
||||
$data['view'] = 'admin';
|
||||
$this->aboRepository->setModel($user_abo);
|
||||
$this->aboRepository->update($data);
|
||||
|
||||
|
|
@ -274,7 +275,7 @@ class AboController extends Controller
|
|||
->orderColumn('status', 'status $1')
|
||||
->orderColumn('active', 'active $1')
|
||||
->orderColumn('is_for', 'is_for $1')
|
||||
->orderColumn('count', 'count $1')
|
||||
->orderColumn('count', '(select count(*) from user_abo_orders where user_abo_orders.user_abo_id = user_abos.id and user_abo_orders.status >= 2) $1')
|
||||
->orderColumn('amount', 'amount $1')
|
||||
->orderColumn('payone_userid', 'payone_userid $1')
|
||||
->rawColumns(['id', 'status', 'active', 'is_for', 'member'])
|
||||
|
|
|
|||
|
|
@ -2,21 +2,23 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
|
||||
|
||||
use Auth;
|
||||
use Carbon;
|
||||
use Request;
|
||||
use App\User;
|
||||
use Validator;
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Mail\UserRestoreEmail;
|
||||
use App\Models\UserAccount;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\GdprDeletionService;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Services\SysLog;
|
||||
use App\Services\UserUtil;
|
||||
use App\Models\UserAccount;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\User;
|
||||
use Auth;
|
||||
use Carbon;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Util;
|
||||
use Illuminate\View\View;
|
||||
use Request;
|
||||
|
||||
class AdminUserController extends Controller
|
||||
{
|
||||
|
|
@ -29,33 +31,35 @@ class AdminUserController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @return Factory|View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$data = [
|
||||
//'values' => User::where('admin', 0)->get(),
|
||||
'values' => User::where('confirmation_code_remider', '!=', 2)->get(),
|
||||
// 'values' => User::where('admin', 0)->get(),
|
||||
'values' => User::where('confirmation_code_remider', '!=', 2)->get(),
|
||||
];
|
||||
|
||||
return view('admin.user.index', $data);
|
||||
}
|
||||
|
||||
public function edit($user_id)
|
||||
{
|
||||
$user = User::findOrFail($user_id);
|
||||
if (!$user->account) {
|
||||
$user->account = new UserAccount();
|
||||
if (! $user->account) {
|
||||
$user->account = new UserAccount;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'user' => $user,
|
||||
];
|
||||
|
||||
return view('admin.user.edit', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
|
||||
* @param Request $request
|
||||
* @return Factory|RedirectResponse|Redirector|View
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
|
|
@ -73,7 +77,7 @@ class AdminUserController extends Controller
|
|||
SysLog::action('save-admin', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user admin value: ' . HTMLHelper::getLabel($user->admin))
|
||||
->setMessage('Set user admin value: '.HTMLHelper::getLabel($user->admin))
|
||||
->save();
|
||||
}
|
||||
|
||||
|
|
@ -81,10 +85,10 @@ class AdminUserController extends Controller
|
|||
$data['confirmed'] = isset($data['confirmed']) ? true : false;
|
||||
$user->confirmed = $data['confirmed'];
|
||||
if ($data['confirmed']) {
|
||||
if (!isset($data['confirmation_date']) || $data['confirmation_date'] == "") {
|
||||
if (! isset($data['confirmation_date']) || $data['confirmation_date'] == '') {
|
||||
$user->confirmation_date = now();
|
||||
} else {
|
||||
$user->confirmation_date = \Carbon::parse(str_replace("- ", "", $data['confirmation_date']));
|
||||
$user->confirmation_date = Carbon::parse(str_replace('- ', '', $data['confirmation_date']));
|
||||
}
|
||||
} else {
|
||||
$user->confirmation_date = null;
|
||||
|
|
@ -92,7 +96,7 @@ class AdminUserController extends Controller
|
|||
SysLog::action('save-confirmed', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user confirmed value: ' . $user->confirmed . " to date: " . $data['confirmation_date'])
|
||||
->setMessage('Set user confirmed value: '.$user->confirmed.' to date: '.$data['confirmation_date'])
|
||||
->save();
|
||||
}
|
||||
|
||||
|
|
@ -103,10 +107,10 @@ class AdminUserController extends Controller
|
|||
$user->wizard = 20;
|
||||
}
|
||||
if ($data['active']) {
|
||||
if (!isset($data['active_date']) || $data['active_date'] == "") {
|
||||
if (! isset($data['active_date']) || $data['active_date'] == '') {
|
||||
$user->active_date = now();
|
||||
} else {
|
||||
$user->active_date = \Carbon::parse(str_replace("- ", "", $data['active_date']));
|
||||
$user->active_date = Carbon::parse(str_replace('- ', '', $data['active_date']));
|
||||
}
|
||||
} else {
|
||||
$user->active_date = null;
|
||||
|
|
@ -114,7 +118,7 @@ class AdminUserController extends Controller
|
|||
SysLog::action('save-active', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user active value: ' . $user->active . " to date: " . $data['active_date'])
|
||||
->setMessage('Set user active value: '.$user->active.' to date: '.$data['active_date'])
|
||||
->save();
|
||||
}
|
||||
|
||||
|
|
@ -122,14 +126,15 @@ class AdminUserController extends Controller
|
|||
$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'] != "") {
|
||||
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']));
|
||||
$payment_account = Carbon::parse(str_replace('- ', '', $data['payment_account']));
|
||||
if ($restore_account) {
|
||||
UserUtil::restoreUser($user, $payment_account);
|
||||
if ($restore_childs) {
|
||||
|
|
@ -137,24 +142,24 @@ class AdminUserController extends Controller
|
|||
}
|
||||
if ($restore_send_email) {
|
||||
$user = User::with('account')->findOrFail($data['id']);
|
||||
Mail::to($user->email)->send(new \App\Mail\UserRestoreEmail($user));
|
||||
Mail::to($user->email)->send(new 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)
|
||||
->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'] == "") {
|
||||
if (! isset($data['payment_account']) || $data['payment_account'] == '') {
|
||||
$user->payment_account = null;
|
||||
} else {
|
||||
$user->wizard = 100;
|
||||
$payment_account = \Carbon::parse(str_replace("- ", "", $data['payment_account']));
|
||||
$payment_account = Carbon::parse(str_replace('- ', '', $data['payment_account']));
|
||||
$user->payment_account = $payment_account;
|
||||
if ($payment_account > Carbon::now()) {
|
||||
if ($user->active === 0) {
|
||||
|
|
@ -168,26 +173,26 @@ class AdminUserController extends Controller
|
|||
}
|
||||
}
|
||||
}
|
||||
//th.schifferegger@gmail.com
|
||||
// th.schifferegger@gmail.com
|
||||
SysLog::action('save-account', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user payment_account from date: ' . $old . " to date: " . $data['payment_account'])
|
||||
->setMessage('Set user payment_account from date: '.$old.' to date: '.$data['payment_account'])
|
||||
->save();
|
||||
}
|
||||
|
||||
if (isset($data['save-shop'])) {
|
||||
$old = $user->getPaymentShopDateFormat(true);
|
||||
if (!isset($data['payment_shop']) || $data['payment_shop'] == "") {
|
||||
if (! isset($data['payment_shop']) || $data['payment_shop'] == '') {
|
||||
$user->payment_shop = null;
|
||||
} else {
|
||||
$user->wizard = 100;
|
||||
$user->payment_shop = \Carbon::parse(str_replace("- ", "", $data['payment_shop']));
|
||||
$user->payment_shop = Carbon::parse(str_replace('- ', '', $data['payment_shop']));
|
||||
}
|
||||
SysLog::action('save-shop', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user payment_shop from date: ' . $old . " to date: " . $data['payment_shop'])
|
||||
->setMessage('Set user payment_shop from date: '.$old.' to date: '.$data['payment_shop'])
|
||||
->save();
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +201,7 @@ class AdminUserController extends Controller
|
|||
SysLog::action('save-test_mode', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user test_mode value: ' . $user->test_mode)
|
||||
->setMessage('Set user test_mode value: '.$user->test_mode)
|
||||
->save();
|
||||
}
|
||||
|
||||
|
|
@ -205,13 +210,14 @@ class AdminUserController extends Controller
|
|||
SysLog::action('save-payment_methods', 'admin_user', 3)
|
||||
->setUserId(Auth::user()->id)
|
||||
->setModel($user->id, User::class)
|
||||
->setMessage('Set user payment_methods value: ' . $user->getPaymentMethodsShort())
|
||||
->setMessage('Set user payment_methods value: '.$user->getPaymentMethodsShort())
|
||||
->save();
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
\Session()->flash('alert-save', true);
|
||||
|
||||
return redirect('/admin/users');
|
||||
}
|
||||
|
||||
|
|
@ -228,15 +234,24 @@ class AdminUserController extends Controller
|
|||
$this->userRepo->deleteUser($user, true);
|
||||
\Session()->flash('alert-success', __('msg.contact_delete'));
|
||||
}
|
||||
if (isset($data['gdpr_delete_user'])) {
|
||||
try {
|
||||
app(GdprDeletionService::class)->deleteUser($user, Auth::id());
|
||||
\Session()->flash('alert-success', __('msg.gdpr_delete_success'));
|
||||
} catch (GdprDeletionException $e) {
|
||||
\Session()->flash('alert-danger', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect('/admin/users');
|
||||
}
|
||||
|
||||
|
||||
public function userLoginAs($userId)
|
||||
{
|
||||
if (Auth::user()->isSuperAdmin()) {
|
||||
$user = User::find($userId);
|
||||
Auth::login($user);
|
||||
|
||||
return redirect('/home');
|
||||
}
|
||||
}
|
||||
|
|
@ -253,7 +268,7 @@ class AdminUserController extends Controller
|
|||
})
|
||||
->with('account')
|
||||
->select('users.*')
|
||||
->where('users.admin', "<", 5);
|
||||
->where('users.admin', '<', 5);
|
||||
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('first_name', function (User $user) {
|
||||
|
|
@ -261,89 +276,101 @@ class AdminUserController extends Controller
|
|||
})
|
||||
->addColumn('email', function (User $user) {
|
||||
if ($user->pre_deleted_at) {
|
||||
return '<span class="badge badge-pill badge-danger">' . $user->email . '</span>';
|
||||
return '<span class="badge badge-pill badge-danger">'.$user->email.'</span>';
|
||||
}
|
||||
|
||||
return $user->email;
|
||||
})
|
||||
->addColumn('last_name', function (User $user) {
|
||||
return $user->account ? $user->account->last_name : '';
|
||||
})
|
||||
->addColumn('id', function (User $user) {
|
||||
return '<a href="' . route('admin_lead_edit', [$user->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
return '<a href="'.route('admin_lead_edit', [$user->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
})
|
||||
->addColumn('admin', function (User $user) {
|
||||
return '<a href="#" data-toggle="modal" data-target="#modals-admin" data-id="' . $user->id . '" data-email="' . $user->email . '" data-admin="' . $user->admin . '">' . HTMLHelper::getRoleLabel($user->admin) . '</a>';
|
||||
return '<a href="#" data-toggle="modal" data-target="#modals-admin" data-id="'.$user->id.'" data-email="'.$user->email.'" data-admin="'.$user->admin.'">'.HTMLHelper::getRoleLabel($user->admin).'</a>';
|
||||
})
|
||||
->addColumn('confirmed', function (User $user) {
|
||||
$date = $user->getConfirmationDateFormat();
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-confirmed" data-id="' . $user->id . '" data-email="' . $user->email . '" data-confirmed="' . $user->confirmed . '" data-confirmation_date="' . $date . '">';
|
||||
return $user->confirmed ? $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>' : $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-confirmed" data-id="'.$user->id.'" data-email="'.$user->email.'" data-confirmed="'.$user->confirmed.'" data-confirmation_date="'.$date.'">';
|
||||
|
||||
return $user->confirmed ? $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> '.$date.'</span></a>' : $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
})
|
||||
->addColumn('active', function (User $user) {
|
||||
if ($user->trashed()) {
|
||||
$date = Carbon::parse(now())->addDays(10)->format('d.m.Y H:i');
|
||||
$email = str_replace("delete-", "", $user->email);
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-restore" data-id="' . $user->id . '" data-email="' . $email . '" data-active="' . $user->active . '" data-payment_account="' . $date . '">';
|
||||
return $link . '<span class="badge badge-pill badge-info"><i class="fa fa-undo"></i> Account reaktivieren</span></a>';
|
||||
$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>';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-active" data-id="'.$user->id.'" data-email="'.$user->email.'" data-active="'.$user->active.'" data-active_date="'.$date.'">';
|
||||
|
||||
return $user->active ? $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> '.$date.'</span></a>' : $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
})
|
||||
->addColumn('account', function (User $user) {
|
||||
$date = $user->getPaymentAccountDateFormat();
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-account" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_account="' . $date . '">';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-account" data-id="'.$user->id.'" data-email="'.$user->email.'" data-payment_account="'.$date.'">';
|
||||
if ($user->payment_account) {
|
||||
if ($user->isActiveAccount()) {
|
||||
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>';
|
||||
return $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> '.$date.'</span></a>';
|
||||
}
|
||||
return $link . '<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> ' . $date . '</span></a>';
|
||||
|
||||
return $link.'<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> '.$date.'</span></a>';
|
||||
}
|
||||
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
|
||||
return $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
})
|
||||
->addColumn('shop', function (User $user) {
|
||||
$date = $user->getPaymentShopDateFormat();
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-shop" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_shop="' . $date . '">';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-shop" data-id="'.$user->id.'" data-email="'.$user->email.'" data-payment_shop="'.$date.'">';
|
||||
if ($user->payment_shop) {
|
||||
if ($user->isActiveShop()) {
|
||||
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $date . '</span></a>';
|
||||
return $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> '.$date.'</span></a>';
|
||||
}
|
||||
return $link . '<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> ' . $date . '</span></a>';
|
||||
|
||||
return $link.'<span class="badge badge-pill badge-warning"><i class="fa fa-ban"></i> '.$date.'</span></a>';
|
||||
}
|
||||
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
|
||||
return $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
})
|
||||
->addColumn('shop_domain', function (User $user) {
|
||||
return $user->shop ? '<a href="' . $user->shop->getSubdomain(false) . '" target="_blank">' . $user->shop->getSubdomain(false) . '</a>' : '';
|
||||
return $user->shop ? '<a href="'.$user->shop->getSubdomain(false).'" target="_blank">'.$user->shop->getSubdomain(false).'</a>' : '';
|
||||
})
|
||||
->addColumn('since', function (User $user) {
|
||||
if ($user->shop) {
|
||||
if ($user->shop->active) {
|
||||
return $user->shop->getActiveDateFormatSmall();
|
||||
}
|
||||
|
||||
return $user->shop->getActiveDateFormatSmall();
|
||||
}
|
||||
return "-";
|
||||
|
||||
return '-';
|
||||
})
|
||||
->addColumn('country', function (User $user) {
|
||||
return ($user->account && $user->account->country) ? $user->account->country->de : '';
|
||||
})
|
||||
->addColumn('my_payment_methods', function (User $user) {
|
||||
$payment_methods = json_encode($user->payment_methods);
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-payment_methods" data-id="' . $user->id . '" data-email="' . $user->email . '" data-payment_methods="' . htmlspecialchars($payment_methods) . '">';
|
||||
if (!$user->payment_methods) {
|
||||
return $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-payment_methods" data-id="'.$user->id.'" data-email="'.$user->email.'" data-payment_methods="'.htmlspecialchars($payment_methods).'">';
|
||||
if (! $user->payment_methods) {
|
||||
return $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
}
|
||||
return $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> ' . $user->getPaymentMethodsShort() . '</span></a>';
|
||||
|
||||
return $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i> '.$user->getPaymentMethodsShort().'</span></a>';
|
||||
})
|
||||
->addColumn('action_login', function (User $user) {
|
||||
return '<a href="' . route('admin_user_login_as', [$user->id]) . '" class="btn icon-btn btn-sm btn-warning" onclick="return confirm(\'' . __('Login as User?') . '\');"><span class="fa fa-sign-in-alt"></span></a>';
|
||||
return '<a href="'.route('admin_user_login_as', [$user->id]).'" class="btn icon-btn btn-sm btn-warning" onclick="return confirm(\''.__('Login as User?').'\');"><span class="fa fa-sign-in-alt"></span></a>';
|
||||
})
|
||||
->addColumn('action_delete', function (User $user) {
|
||||
return '<a class="btn icon-btn btn-sm btn-danger" href="#" data-toggle="modal" data-target="#modals-user-delete" data-id="' . $user->id . '" data-email="' . $user->email . '"><span class="fa fa-trash"></span></a>';
|
||||
return '<a class="btn icon-btn btn-sm btn-danger" href="#" data-toggle="modal" data-target="#modals-user-delete" data-id="'.$user->id.'" data-email="'.$user->email.'"><span class="fa fa-trash"></span></a>';
|
||||
})
|
||||
->addColumn('test_mode', function (User $user) {
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-test_mode" data-id="' . $user->id . '" data-email="' . $user->email . '" data-test_mode="' . $user->test_mode . '">';
|
||||
return $user->test_mode ? $link . '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span></a>' : $link . '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
$link = '<a href="#" data-toggle="modal" data-target="#modals-test_mode" data-id="'.$user->id.'" data-email="'.$user->email.'" data-test_mode="'.$user->test_mode.'">';
|
||||
|
||||
return $user->test_mode ? $link.'<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span></a>' : $link.'<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span></a>';
|
||||
})
|
||||
->orderColumn('id', 'id $1')
|
||||
->orderColumn('email', 'email $1')
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Session;
|
||||
use \SoapClient;
|
||||
|
||||
class KasController extends Controller
|
||||
{
|
||||
|
||||
|
||||
// Logindaten
|
||||
private $kas_user = 'w017f6e4'; // KAS-Logon
|
||||
private $kas_pass = 'Medxiz-funteb-7dubdi'; // KAS-Passwort
|
||||
private $session_lifetime = 600; // Gültigkeit des Tokens in Sek. bis zur neuen Authentifizierung
|
||||
private $session_update_lifetime = 'Y'; // bei N läuft die Session nach <$session_lifetime> Sekunden ab, bei Y verlängert sich die Session mit jeder Benutzung
|
||||
private $CredentialToken = false;
|
||||
private $kas_flood_delay = 2;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->login();
|
||||
}
|
||||
|
||||
|
||||
public function action($func, $para = array()){
|
||||
|
||||
$this->checkSession($func);
|
||||
try
|
||||
{
|
||||
$Params = array(); // Parameter für die API-Funktion
|
||||
$SoapRequest = new SoapClient('https://kasapi.kasserver.com/soap/wsdl/KasApi.wsdl', [
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
'exceptions' => false
|
||||
]);
|
||||
$req = $SoapRequest->KasApi(json_encode(array(
|
||||
'KasUser' => $this->kas_user, // KAS-User
|
||||
'KasAuthType' => 'session', // Auth per Sessiontoken
|
||||
'KasAuthData' => $this->CredentialToken, // Auth-Token
|
||||
'KasRequestType' => $func, // API-Funktion
|
||||
'KasRequestParams' => $para // Parameter an die API-Funktion
|
||||
)));
|
||||
Session::put('flood_protection.'.$func, time() + $this->kas_flood_delay + 0.2);
|
||||
|
||||
if(is_array($req) && isset($req['Response']['ReturnString']) && $req['Response']['ReturnString'] == "TRUE"){
|
||||
return $req['Response']['ReturnInfo'];
|
||||
}
|
||||
return $req;
|
||||
}
|
||||
|
||||
// Fehler abfangen und ausgeben
|
||||
catch (\SoapFault $fault)
|
||||
{
|
||||
trigger_error(" Fehlernummer: {$fault->faultcode},
|
||||
Fehlermeldung: {$fault->faultstring},
|
||||
Verursacher: {$fault->faultactor},
|
||||
Details: {$fault->detail}", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function login(){
|
||||
|
||||
$this->checkSession('auth');
|
||||
try
|
||||
{
|
||||
|
||||
$SoapLogon = new SoapClient('https://kasapi.kasserver.com/soap/wsdl/KasAuth.wsdl', [
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
'exceptions' => false
|
||||
]);
|
||||
$this->CredentialToken = $SoapLogon->KasAuth(json_encode(array(
|
||||
'KasUser' => $this->kas_user,
|
||||
'KasAuthType' => 'plain',
|
||||
'KasPassword' => $this->kas_pass,
|
||||
'SessionLifeTime' => $this->session_lifetime,
|
||||
'SessionUpdateLifeTime' => $this->session_update_lifetime
|
||||
)));
|
||||
Session::put('flood_protection.auth', time() + $this->kas_flood_delay + 0.2);
|
||||
|
||||
}
|
||||
|
||||
// Fehler abfangen und ausgeben
|
||||
catch (\SoapFault $fault)
|
||||
{
|
||||
trigger_error("Fehlernummer: {$fault->faultcode},
|
||||
Fehlermeldung: {$fault->faultstring},
|
||||
Verursacher: {$fault->faultactor},
|
||||
Details: {$fault->detail}", E_USER_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function checkSession($func)
|
||||
{
|
||||
$name = 'flood_protection.'.$func;
|
||||
|
||||
if(Session::exists($name)){
|
||||
$time_to_wait = (float)Session::get($name) - time();
|
||||
Session::forget($name);
|
||||
}else {
|
||||
$time_to_wait = 0;
|
||||
}
|
||||
if ( $time_to_wait >= 0 ) {
|
||||
usleep( intval( $time_to_wait*1000000 ) );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class KasSLLController extends Controller
|
||||
{
|
||||
|
||||
private static $ssl_certificate_sni_csr = "";
|
||||
private static $ssl_certificate_sni_key = "-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgohGr2e3ysw/Awvzh
|
||||
qkqDS4iQgRvWwNIYxTcPxpdcndGhRANCAASZjlV2bQbLQrOveMlYOowR3IlfND7z
|
||||
OxauFGabhvWSU1cg2w4U4bu/QXnDXfHHkcLp4M5WgHzX9Nw2m/abyJJ6
|
||||
-----END PRIVATE KEY-----";
|
||||
private static $ssl_certificate_sni_crt = "-----BEGIN CERTIFICATE-----
|
||||
MIIEpDCCBEqgAwIBAgIQVIm0T0SQ6D20YQxMaHEKbDAKBggqhkjOPQQDAjCBjzEL
|
||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
||||
BxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5T
|
||||
ZWN0aWdvIEVDQyBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMB4X
|
||||
DTI0MDgwMTAwMDAwMFoXDTI1MDkwMTIzNTk1OVowGDEWMBQGA1UEAwwNKi5taXZp
|
||||
dGEuY2FyZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJmOVXZtBstCs694yVg6
|
||||
jBHciV80PvM7Fq4UZpuG9ZJTVyDbDhThu79BecNd8ceRwungzlaAfNf03Dab9pvI
|
||||
knqjggL8MIIC+DAfBgNVHSMEGDAWgBT2hQo7EYbhBH0Oqgss0u7MZHt7rjAdBgNV
|
||||
HQ4EFgQUVCkHH2AasJQFWFs63rdcb6BRvyowDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
|
||||
EwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARC
|
||||
MEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdv
|
||||
LmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4MHYwTwYIKwYBBQUHMAKG
|
||||
Q2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb0VDQ0RvbWFpblZhbGlkYXRp
|
||||
b25TZWN1cmVTZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl
|
||||
Y3RpZ28uY29tMCUGA1UdEQQeMByCDSoubWl2aXRhLmNhcmWCC21pdml0YS5jYXJl
|
||||
MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDd3Mo0ldfhFgXnlTL6x5/4PRxQ
|
||||
39sAOhQSdgosrLvIKgAAAZEMky0iAAAEAwBHMEUCICSH9TLHP8tqMyBTBpxF1+lw
|
||||
4wAnWf4E5pPJ6651S8P9AiEAkKqOQDaVdoFI1+jM28grXnG5o0vFLUwa0o49KYQ3
|
||||
k+sAdgAN4fIwK9MNwUBiEgnqVS78R3R8sdfpMO8OQh60fk6qNAAAAZEMkyzbAAAE
|
||||
AwBHMEUCIFJfJS4cojUm9nHQ1TVlxpFwOV7QwCj9MOfq0CCkVzsGAiEA8WQrE1ri
|
||||
kJkeIVPSgUVJpIz8TKef2aR+Ivzkzon52QIAdgAS8U40vVNyTIQGGcOPP3oT+Oe1
|
||||
YoeInG0wBYTr5YYmOgAAAZEMkyzBAAAEAwBHMEUCIQCH8/qTmCNea3FdBVk0c3Wu
|
||||
FrvYnoQlTQaaDS/zeTxSzwIge6VO5Aeor30Wu675zBYzNsIru5gXOTl4dteBMYnC
|
||||
0JswCgYIKoZIzj0EAwIDSAAwRQIhAKxmgpPqW6UAcWHCoWAPN673pBMxnCKn3vFq
|
||||
wUkhGrT7AiBDUsDuMhabsGlZ10X2GXcm+1mwxdMLSDYEWiwk5fUaNA==
|
||||
-----END CERTIFICATE-----";
|
||||
private static $ssl_certificate_sni_bundle = "-----BEGIN CERTIFICATE-----
|
||||
MIIDqDCCAy6gAwIBAgIRAPNkTmtuAFAjfglGvXvh9R0wCgYIKoZIzj0EAwMwgYgx
|
||||
CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJz
|
||||
ZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQD
|
||||
EyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE4MTEw
|
||||
MjAwMDAwMFoXDTMwMTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAkdCMRswGQYDVQQI
|
||||
ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT
|
||||
D1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGlnbyBFQ0MgRG9tYWluIFZh
|
||||
bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABHkYk8qfbZ5sVwAjBTcLXw9YWsTef1Wj6R7W2SUKiKAgSh16TwUwimNJE4xk
|
||||
IQeV/To14UrOkPAY9z2vaKb71EijggFuMIIBajAfBgNVHSMEGDAWgBQ64QmG1M8Z
|
||||
wpZ2dEl23OA1xmNjmjAdBgNVHQ4EFgQU9oUKOxGG4QR9DqoLLNLuzGR7e64wDgYD
|
||||
VR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEwUAYD
|
||||
VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz
|
||||
dEVDQ0NlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/
|
||||
BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdEVD
|
||||
Q0FkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1
|
||||
c3QuY29tMAoGCCqGSM49BAMDA2gAMGUCMEvnx3FcsVwJbZpCYF9z6fDWJtS1UVRs
|
||||
cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9
|
||||
4ToKFbZl+/OnFFzqnvOhcjHvClECEQcKmc8fmA==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7
|
||||
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
|
||||
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
|
||||
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
|
||||
MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
|
||||
MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
|
||||
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL
|
||||
q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc
|
||||
JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA
|
||||
FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1
|
||||
xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI
|
||||
MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j
|
||||
b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG
|
||||
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM
|
||||
BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy
|
||||
ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+
|
||||
FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV
|
||||
bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY
|
||||
CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf
|
||||
8qn0dNW44bOwgeThpWOjzOoEeJBuv/c=
|
||||
-----END CERTIFICATE-----";
|
||||
|
||||
|
||||
public static function getApiSSLParameter(){
|
||||
|
||||
return [
|
||||
'ssl_certificate_is_active' => "Y",
|
||||
'ssl_certificate_sni_csr' => self::$ssl_certificate_sni_csr,
|
||||
'ssl_certificate_sni_key' => self::$ssl_certificate_sni_key,
|
||||
'ssl_certificate_sni_crt' => self::$ssl_certificate_sni_crt,
|
||||
'ssl_certificate_sni_bundle' => self::$ssl_certificate_sni_bundle,
|
||||
'ssl_certificate_force_https' => "Y",
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPara(){
|
||||
return [
|
||||
"ssl_proxy" => "N",
|
||||
"ssl_certificate_ip" => "N",
|
||||
"ssl_certificate_sni" => "Y",
|
||||
"ssl_certificate_sni_csr" => "-----BEGIN CERTIFICATE REQUEST-----\n
|
||||
MIIC0DCCAbgCAQAwgYoxCzAJBgNVBAYTAkRFMQ4wDAYDVQQRDAU4Nzc1NTEPMA0G\n
|
||||
A1UECAwGQmF5ZXJuMRUwEwYDVQQHDAxLaXJjaGhhc2xhY2gxEzARBgNVBAkMCkxl\n
|
||||
aW5mZWxkIDIxFjAUBgNVBAoMDXJpd2EtdGVjIGUuSy4xFjAUBgNVBAMMDSoubWl2\n
|
||||
aXRhLmNhcmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVOhtOTJBn\n
|
||||
5V9SmHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veemfY1j\n
|
||||
6rXq7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj86TT\n
|
||||
Abq5wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9Q2Fb\n
|
||||
UKEfiEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXorW6X\n
|
||||
aifb1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1FOSc\n
|
||||
WlN+M/+TyUybAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAJeDEZBjk9ITfZAzJ\n
|
||||
LMVIsu4Cuz2YZkZY8r+Wdd8E1k0lAdcht2xY/uL91NwXl/hUJiVo4uBUFnCogc/k\n
|
||||
dAxrRsrjiw8nHgfBgreGZj73S+tx00DUz1eP9uIVNzSO+aRMBHL8BvvLUR94KVSu\n
|
||||
aVhy8fJESdDiF5TwZR7jPIWoU0esI1cEebFG2kS/wTSuUWxLh1ZGGuEKFETfEpOK\n
|
||||
ooy0gUcHTP1NWo/vTDwdlf47t2vvZ/ZD0ursWXp6CNNZvwimHPxgSq8KKxLQyf5U\n
|
||||
S/UHogxC8PbOzTJI0DutkCZO0iUO8gTq0GXZHVqkqTCixfIFeuMuL0ZvXYJVhZXP\n
|
||||
4CBn5g==\n
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
",
|
||||
"ssl_certificate_sni_key" => "-----BEGIN PRIVATE KEY-----\n
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVOhtOTJBn5V9S\n
|
||||
mHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veemfY1j6rXq\n
|
||||
7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj86TTAbq5\n
|
||||
wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9Q2FbUKEf\n
|
||||
iEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXorW6Xaifb\n
|
||||
1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1FOScWlN+\n
|
||||
M/+TyUybAgMBAAECggEAJ0hYj9AP44m6AiApRpbCdPiLhZmx3ANfrOJpi1dc2BqD\n
|
||||
pIzCePOXlnh+6fMV0Cn7uY60QFuksLzEjsdBXLtgQYvuGu1plSZT/5VAA4RnhYpJ\n
|
||||
7O+tnvFt00k/iCi/bWmCXY4kCvrEVNeLtALoa9znOVMhiBtGGiFxO3iQ+y7jxF6J\n
|
||||
49O99G8gPGjMm/BdFjnBpUZ+Z5ZGXvrKTZaQRDE5HXEM8dUTBXPL4+dMdfQIiyKZ\n
|
||||
pNklwkMjS4/LY6xDP16Wj25bSq5W9WSlTja/ZJ2eKqr6c7WxKP6TvjGh9FMkIUps\n
|
||||
Bl9BNKmgixgiHVq/4WwUSZ1PAEuGQJiptVdeJcgioQKBgQDDdNaRg6Z5yVk+UjXw\n
|
||||
DHJkUmquowijJUG/2seLYMFm1lkr9xbGvfGfnOSr79jim3haL/qichWh++QjeBsM\n
|
||||
fwBPMbRY+JNMHpaDpvHAI2YNqXP+rBr4pJnICrHoqIzVqxbDJ04LQZBRD10cTlFz\n
|
||||
+l+Ok60XTAX/wlKN96BnjuOVXQKBgQDDc2aoU37E4wPYNXcMLvoDv3+Zq3KCEMQD\n
|
||||
gtNgSbyd37Dw8n35TGWubFLsvYnPLBebB6wAgTPzvTpJmPTr7nKUJsd4rbfvuh+i\n
|
||||
vVhH/2xq70Pi1XqvQkmo+H1OJX+t2n/Hxr7TQGkqVI9eNfvW8UP+TGPjxGIw8Y0b\n
|
||||
6t8Ky6USVwKBgQCszV5qVh9Xqtj4zUwch5SW93qUHVWkj2rayP0ET62NUtKRmSmM\n
|
||||
2h+GAvr0u99fMR6tdZ+8AOr5RC7F4Qjg+mN2oLYWtuXbNWvSx0USnvk5+Oexb82E\n
|
||||
qFnBTxtNW77vpQxByz0nnHaQA+pI/UDsLZ5P+mXco/zlypKcKyKoi97PjQKBgDQV\n
|
||||
9+CZx6m+edLPhLc5eaUwDlgsaWqh/yqUXbJGVD6aUzQS22Fpa5uNAJhYdnZAYNYO\n
|
||||
uFa2F9s3rWXZnkOVmvFCWFwfp2n6Zt3eqb0eI41nz+aOT5CPEMQ33GTL93ekR/M8\n
|
||||
UrRHcP8347EOn9uLFjyZrPEQ773tUVaERAZDeO0nAoGAZXMhlmKmqTrM2jSb64ja\n
|
||||
pEddcEW2LuTvwQueOKUuSSwmCydKXkcgrYZ4EHyOgvVN9JZ5ZfW6ZathFipVEKdy\n
|
||||
diQ860kC4h++erAa8dvB1DUG5oldYYPiEKOyyyn+tNU298QcEkLrG1JcLuUXpfTg\n
|
||||
8dPIr+VpGomsvpwGTfJFjlE=\n
|
||||
-----END PRIVATE KEY-----\n
|
||||
",
|
||||
"ssl_certificate_sni_crt" => "-----BEGIN CERTIFICATE-----\n
|
||||
MIIGLzCCBRegAwIBAgIRAJ6HzyfKXWCtRn3q9gGkgYEwDQYJKoZIhvcNAQELBQAw\n
|
||||
gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\n
|
||||
BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE\n
|
||||
AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD\n
|
||||
QTAeFw0yMTA3MjIwMDAwMDBaFw0yMjA3MjIyMzU5NTlaMBgxFjAUBgNVBAMMDSou\n
|
||||
bWl2aXRhLmNhcmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVOhtO\n
|
||||
TJBn5V9SmHmo/EawNiO0VwHOVnnrfnaPD2A1DeKqHmAfMTaybHaCfi+mufV8veem\n
|
||||
fY1j6rXq7RFU46SMBbFlfZqKS/3zb2d3yRT7OBU83PV5P8JXHrqEArlmKiOZcPoj\n
|
||||
86TTAbq5wwxjFXkePzJSdOdUN/Z1E1tI8ieUQC40tpMsRvf5XOzQZousXBT1P6F9\n
|
||||
Q2FbUKEfiEBJ0wjnz74a73U7DebuYGEFPSjVjrkVB11+55y1MBkwg/6JIro+BlXo\n
|
||||
rW6Xaifb1PKFbTFQnlC4BAKyPHxNKWZCSHgw/C3A7fBQKHM1wVhZo2BZrumdE+X1\n
|
||||
FOScWlN+M/+TyUybAgMBAAGjggL6MIIC9jAfBgNVHSMEGDAWgBSNjF7EVK2K4Xfp\n
|
||||
m/mbBeG4AY1h4TAdBgNVHQ4EFgQUCS0Y1v7p19isO7cTuP3YrKVr2OcwDgYDVR0P\n
|
||||
AQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\n
|
||||
AQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYX\n
|
||||
aHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4\n
|
||||
MHYwTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1JT\n
|
||||
QURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGG\n
|
||||
F2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCUGA1UdEQQeMByCDSoubWl2aXRhLmNh\n
|
||||
cmWCC21pdml0YS5jYXJlMIIBfAYKKwYBBAHWeQIEAgSCAWwEggFoAWYAdQBGpVXr\n
|
||||
dfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXrNeYDBAAAEAwBGMEQCIFzd\n
|
||||
+zLvEGolSmSaa7vaQxv63DuX5vHQggER6/Dh+jZGAiAcUn8AZjF7GQOd4LTzGMhU\n
|
||||
KsGNyn6d3n4cJ9fy9BzRxAB1AEHIyrHfIkZKEMahOglCh15OMYsbA+vrS8do8JBi\n
|
||||
lgb2AAABes15gIYAAAQDAEYwRAIgE0NFzvN7qEre8Bc1C8EsMHD+5PDyQHZRBJkN\n
|
||||
OdxsH9MCIDBSFFZTheD2+nzbHm5WLvAI75xyUvyBx/LEy3XBtjulAHYAKXm+8J45\n
|
||||
OSHwVnOfY6V35b5XfZxgCvj5TV0mXCVdx4QAAAF6zXmAWwAABAMARzBFAiAbRPVk\n
|
||||
w3AIzVF7gE0R3ZJgou7P4o9KL2yRgAaeGbbClgIhAPL86sD0GwPZ9ZsL31q07Y/S\n
|
||||
1kq5ohBt907fOisMwI0HMA0GCSqGSIb3DQEBCwUAA4IBAQAaYeV2NtUM2HkxWbfd\n
|
||||
3jVAs1PdBIYtktBpx7UwNphylqF4qlsZwV5XZxeD/K7mTW5tgNaHHrEjaOME/y1s\n
|
||||
rWTIt1D+UUmDdiSgKfVF5gfajPFVepOcb5OC+ielevvnVJn/6Tqa/RNz0GstwMnB\n
|
||||
3lBaoP7oGuBy2Ow3LG0+yO4Q0j82gIkOM15CsjY9ZK540HAXllxKGN29Yf+RDkqE\n
|
||||
zRk4TE12MEW+Ugw6RxDSUCfKmev4iUAT9vq790OESAfOKY1zg/6hIF3noH1IFt1d\n
|
||||
e0wVWz58KTXBqHsmxX3F1PUuT6NY+wRsVfnc8hR8mfJibJ0VL8wxjzScDXyHpZr/\n
|
||||
o3I7\n
|
||||
-----END CERTIFICATE-----
|
||||
",
|
||||
"ssl_certificate_sni_bundle" => "-----BEGIN CERTIFICATE-----\n
|
||||
MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB\n
|
||||
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n
|
||||
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n
|
||||
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx\n
|
||||
MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV\n
|
||||
BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE\n
|
||||
ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g\n
|
||||
VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC\n
|
||||
AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N\n
|
||||
TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj\n
|
||||
eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E\n
|
||||
oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk\n
|
||||
Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY\n
|
||||
uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j\n
|
||||
BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb\n
|
||||
+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G\n
|
||||
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw\n
|
||||
CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0\n
|
||||
LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr\n
|
||||
BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv\n
|
||||
bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov\n
|
||||
L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H\n
|
||||
ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH\n
|
||||
7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi\n
|
||||
H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx\n
|
||||
RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv\n
|
||||
xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38\n
|
||||
sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL\n
|
||||
l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq\n
|
||||
6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY\n
|
||||
LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5\n
|
||||
yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K\n
|
||||
00u/I5sUKUErmgQfky3xxzlIPK1aEn8=\n
|
||||
-----END CERTIFICATE-----\n
|
||||
-----BEGIN CERTIFICATE-----\n
|
||||
MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7\n
|
||||
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD\n
|
||||
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE\n
|
||||
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4\n
|
||||
MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5\n
|
||||
MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO\n
|
||||
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0\n
|
||||
aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI\n
|
||||
s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG\n
|
||||
vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ\n
|
||||
Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb\n
|
||||
IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0\n
|
||||
tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E\n
|
||||
xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV\n
|
||||
icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5\n
|
||||
D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ\n
|
||||
WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ\n
|
||||
5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG\n
|
||||
KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg\n
|
||||
EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID\n
|
||||
ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG\n
|
||||
BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t\n
|
||||
L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr\n
|
||||
BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA\n
|
||||
A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+\n
|
||||
rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+\n
|
||||
/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA\n
|
||||
CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F\n
|
||||
zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA\n
|
||||
vGp4z7h/jnZymQyd/teRCBaho1+V\n
|
||||
-----END CERTIFICATE-----
|
||||
",
|
||||
"ssl_certificate_sni_chainfile" => null,
|
||||
"ssl_certificate_sni_force_https" => "N",
|
||||
"ssl_certificate_sni_hsts_max_age" => "-1"
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,7 +11,9 @@ use App\Models\ShoppingOrder;
|
|||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\CustomerPriority;
|
||||
use App\Services\Shop;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Yard;
|
||||
|
|
@ -28,7 +30,7 @@ class ShoppingUserController extends Controller
|
|||
/**
|
||||
* @param Request $request
|
||||
* wp_order_numbers[1234, 1234]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function status(Request $request)
|
||||
{
|
||||
|
|
@ -74,7 +76,7 @@ class ShoppingUserController extends Controller
|
|||
/**
|
||||
* @param Request $request
|
||||
* wp_order_number [1234]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
|
|
@ -126,7 +128,7 @@ class ShoppingUserController extends Controller
|
|||
/**
|
||||
* @param Request $request
|
||||
* wp_order_number [1234]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function open(Request $request)
|
||||
{
|
||||
|
|
@ -178,7 +180,7 @@ class ShoppingUserController extends Controller
|
|||
/**
|
||||
* @param Request $request
|
||||
* wp_order_numbers [1234, 1234]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
|
|
@ -229,7 +231,7 @@ class ShoppingUserController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
|
|
@ -258,7 +260,7 @@ class ShoppingUserController extends Controller
|
|||
|
||||
// Kundenhoheit prüfen
|
||||
$priority = CustomerPriority::checkOne($shopping_user, true, false, true);
|
||||
\App\Services\Shop::newUserOrder($shopping_user->number);
|
||||
Shop::newUserOrder($shopping_user->number);
|
||||
// exists //like //update
|
||||
$user = $this->prepareForShow($shopping_user);
|
||||
|
||||
|
|
@ -277,7 +279,7 @@ class ShoppingUserController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
|
|
@ -297,7 +299,7 @@ class ShoppingUserController extends Controller
|
|||
// Kundenhoheit prüfen
|
||||
$priority = CustomerPriority::checkChangeOne($shopping_user, $data, true);
|
||||
$updated = $shopping_user->fill($data)->save();
|
||||
\App\Services\Shop::newUserOrder($shopping_user->number);
|
||||
Shop::newUserOrder($shopping_user->number);
|
||||
|
||||
if ($updated) {
|
||||
$user = $this->prepareForShow($shopping_user);
|
||||
|
|
@ -610,6 +612,12 @@ class ShoppingUserController extends Controller
|
|||
|
||||
$ShippingCountry = ShippingCountry::whereCountryId($shopping_user->shipping_country_id)->first();
|
||||
if ($ShippingCountry) {
|
||||
Yard::instance('shopping')->setUserPriceInfos([
|
||||
'shipping_free' => $ShippingCountry->shipping?->free ?? false,
|
||||
'user_tax_free' => false,
|
||||
'user_reverse_charge' => false,
|
||||
'user_country_id' => $shopping_user->billing_country_id,
|
||||
]);
|
||||
Yard::instance('shopping')->setShippingCountryWithPrice($ShippingCountry->id);
|
||||
}
|
||||
$shopping_order = $this->makeShoppingOrder($shopping_user, $wp_invoice_path, $api_notice);
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Repositories\CustomerRepository;
|
||||
use App\Services\CustomerPriority;
|
||||
use App\Services\GdprDeletionService;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Services\Shop;
|
||||
use Auth;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
protected $customerRepository;
|
||||
|
|
@ -17,6 +20,7 @@ class CustomerController extends Controller
|
|||
public function __construct(CustomerRepository $customerRepository)
|
||||
{
|
||||
$this->middleware('admin');
|
||||
$this->middleware('superadmin')->only('gdprDelete');
|
||||
$this->customerRepository = $customerRepository;
|
||||
}
|
||||
|
||||
|
|
@ -25,16 +29,17 @@ class CustomerController extends Controller
|
|||
if (Request::get('reset') === 'filter') {
|
||||
set_user_attr('filter_member_id', null);
|
||||
set_user_attr('filter_customer_member', null);
|
||||
|
||||
return redirect(route('admin_customers'));
|
||||
}
|
||||
$filter_members = ShoppingUser::join('users', 'member_id', '=', 'users.id')->groupBy('member_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get(); //->pluck('email', 'id')->unique()->toArray();
|
||||
$filter_members = ShoppingUser::join('users', 'member_id', '=', 'users.id')->groupBy('member_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get(); // ->pluck('email', 'id')->unique()->toArray();
|
||||
$data = [
|
||||
'filter_members' => $filter_members,
|
||||
];
|
||||
|
||||
return view('admin.customer.index', $data);
|
||||
}
|
||||
|
||||
|
||||
public function detail($id)
|
||||
{
|
||||
$shopping_user = ShoppingUser::findOrFail($id);
|
||||
|
|
@ -43,6 +48,7 @@ class CustomerController extends Controller
|
|||
'isAdmin' => true,
|
||||
'isView' => 'customer',
|
||||
];
|
||||
|
||||
return view('admin.customer.detail', $data);
|
||||
}
|
||||
|
||||
|
|
@ -50,20 +56,44 @@ class CustomerController extends Controller
|
|||
{
|
||||
$shopping_user = ShoppingUser::findOrFail($id);
|
||||
$result = $this->customerRepository->deleteCustomer($shopping_user);
|
||||
if (!$result) {
|
||||
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 gdprDelete($id)
|
||||
{
|
||||
$shopping_user = ShoppingUser::withTrashed()->findOrFail($id);
|
||||
|
||||
if (! Request::get('gdpr_delete_customer')) {
|
||||
\Session()->flash('alert-danger', __('msg.gdpr_confirm_checkbox_missing'));
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
||||
try {
|
||||
app(GdprDeletionService::class)->deleteShoppingUser($shopping_user, Auth::id());
|
||||
\Session()->flash('alert-success', __('msg.gdpr_delete_customer_success'));
|
||||
} catch (GdprDeletionException $e) {
|
||||
\Session()->flash('alert-danger', $e->getMessage());
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
||||
return redirect(route('admin_customers'));
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
if ($id === "new") {
|
||||
$shopping_user = new ShoppingUser();
|
||||
$shopping_user->id = "new";
|
||||
if ($id === 'new') {
|
||||
$shopping_user = new ShoppingUser;
|
||||
$shopping_user->id = 'new';
|
||||
} else {
|
||||
$shopping_user = ShoppingUser::findOrFail($id);
|
||||
}
|
||||
|
|
@ -72,6 +102,7 @@ class CustomerController extends Controller
|
|||
'isAdmin' => true,
|
||||
'isView' => 'customer',
|
||||
];
|
||||
|
||||
return view('admin.customer.edit', $data);
|
||||
}
|
||||
|
||||
|
|
@ -80,23 +111,25 @@ class CustomerController extends Controller
|
|||
$data = Request::all();
|
||||
|
||||
if ($data['action'] === 'shopping-user-change-member') {
|
||||
if (!isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
|
||||
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
|
||||
$data = [
|
||||
'change_member_error' => "Das Passwort ist falsch.",
|
||||
'change_member_error' => 'Das Passwort ist falsch.',
|
||||
'shopping_user' => ShoppingUser::find($id),
|
||||
'isAdmin' => true,
|
||||
'isView' => 'customer',
|
||||
];
|
||||
|
||||
return view('admin.customer.detail', $data);
|
||||
}
|
||||
//change
|
||||
// change
|
||||
$shopping_user = ShoppingUser::findOrFail($data['id']);
|
||||
CustomerPriority::newMemberForCustomer($shopping_user, $data['change_member_id'], $data['customer_set_member_for']);
|
||||
\Session()->flash('alert-save', true);
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
if ($data['action'] === 'shopping-user-store') {
|
||||
$rules = array(
|
||||
$rules = [
|
||||
'billing_salutation' => 'required',
|
||||
'billing_firstname' => 'required',
|
||||
'billing_lastname' => 'required',
|
||||
|
|
@ -104,10 +137,10 @@ class CustomerController extends Controller
|
|||
'billing_address' => 'required',
|
||||
'billing_zipcode' => 'required',
|
||||
'billing_city' => 'required',
|
||||
'billing_country_id' => 'required'
|
||||
);
|
||||
'billing_country_id' => 'required',
|
||||
];
|
||||
|
||||
if (!Request::get('same_as_billing')) {
|
||||
if (! Request::get('same_as_billing')) {
|
||||
$rules = array_merge($rules, [
|
||||
'shipping_firstname' => 'required',
|
||||
'shipping_lastname' => 'required',
|
||||
|
|
@ -115,7 +148,7 @@ class CustomerController extends Controller
|
|||
'shipping_zipcode' => 'required',
|
||||
'shipping_city' => 'required',
|
||||
'shipping_salutation' => 'required',
|
||||
'shipping_country_id' => 'required'
|
||||
'shipping_country_id' => 'required',
|
||||
]);
|
||||
}
|
||||
$validator = Validator::make(Request::all(), $rules);
|
||||
|
|
@ -128,7 +161,7 @@ class CustomerController extends Controller
|
|||
$data['language'] = isset($data['language']) ? $data['language'] : \App::getLocale();
|
||||
$data['has_buyed'] = isset($data['has_buyed']) ? true : false;
|
||||
$data['subscribed'] = isset($data['subscribed']) ? true : false;
|
||||
//subscribed can only true when has_buyed ist active
|
||||
// subscribed can only true when has_buyed ist active
|
||||
$data['subscribed'] = $data['has_buyed'] ? $data['subscribed'] : false;
|
||||
|
||||
/* if($shopping_user->auth_user_id > 0){
|
||||
|
|
@ -140,21 +173,23 @@ class CustomerController extends Controller
|
|||
CustomerPriority::checkChangeOne($shopping_user, $data, true);
|
||||
$shopping_user->fill($data);
|
||||
$shopping_user->save();
|
||||
\App\Services\Shop::newUserOrder($shopping_user->number);
|
||||
Shop::newUserOrder($shopping_user->number);
|
||||
|
||||
\Session()->flash('alert-save', true);
|
||||
}
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
||||
public function getCustomers()
|
||||
{
|
||||
$query = ShoppingUser::select('shopping_users.*')->where('auth_user_id', '=', NULL);
|
||||
$query = ShoppingUser::select('shopping_users.*')->where('auth_user_id', '=', null);
|
||||
|
||||
set_user_attr('filter_member_id', Request::get('filter_member_id'));
|
||||
if (Request::get('filter_member_id') != "") {
|
||||
if (Request::get('filter_member_id') != '') {
|
||||
$query->where('member_id', '=', Request::get('filter_member_id'));
|
||||
}
|
||||
|
||||
/* set_user_attr('filter_customer_member', Request::get('filter_customer_member'));
|
||||
if(Request::get('filter_customer_member') != ""){
|
||||
if(Request::get('filter_customer_member') === 'customers'){
|
||||
|
|
@ -166,10 +201,10 @@ class CustomerController extends Controller
|
|||
}*/
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('id', function (ShoppingUser $ShoppingUser) {
|
||||
return '<a href="' . route('admin_customer_detail', [$ShoppingUser->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
return '<a href="'.route('admin_customer_detail', [$ShoppingUser->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
})
|
||||
->addColumn('billing_email', function (ShoppingUser $ShoppingUser) {
|
||||
return $ShoppingUser->faker_mail ? "-" : $ShoppingUser->billing_email;
|
||||
return $ShoppingUser->faker_mail ? '-' : $ShoppingUser->billing_email;
|
||||
})
|
||||
->addColumn('billing_salutation', function (ShoppingUser $ShoppingUser) {
|
||||
return HTMLHelper::getSalutationLang($ShoppingUser->billing_salutation);
|
||||
|
|
@ -178,19 +213,19 @@ class CustomerController extends Controller
|
|||
return $ShoppingUser->billing_country ? $ShoppingUser->billing_country->getLocated() : '';
|
||||
})
|
||||
->addColumn('isMember', function (ShoppingUser $ShoppingUser) {
|
||||
return get_active_badge($ShoppingUser->auth_user_id) . ($ShoppingUser->mode === 'dev' ? ' <span class="badge badge-warning">dev</span>' : '');
|
||||
return get_active_badge($ShoppingUser->auth_user_id).($ShoppingUser->mode === 'dev' ? ' <span class="badge badge-warning">dev</span>' : '');
|
||||
})
|
||||
->addColumn('member_id', function (ShoppingUser $ShoppingUser) {
|
||||
if ($ShoppingUser->is_like) {
|
||||
return '<button type="button" class="btn btn-xs btn-outline-info" data-toggle="modal" data-target="#modals-load-content"
|
||||
data-id="' . $ShoppingUser->id . '"
|
||||
data-id="'.$ShoppingUser->id.'"
|
||||
data-action="shopping-user-is-like-member"
|
||||
data-back="' . route('admin_customers') . '"
|
||||
data-back="'.route('admin_customers').'"
|
||||
data-modal="modal-xl"
|
||||
data-route="' . route('modal_load') . '"><span class="fa fa-edit"></span> Berater zuordnen</button>';
|
||||
data-route="'.route('modal_load').'"><span class="fa fa-edit"></span> Berater zuordnen</button>';
|
||||
}
|
||||
if ($ShoppingUser->member) {
|
||||
return '<a href="' . route('admin_lead_edit', [$ShoppingUser->member_id]) . '">' . $ShoppingUser->member->getFullName() . '</a>';
|
||||
return '<a href="'.route('admin_lead_edit', [$ShoppingUser->member_id]).'">'.$ShoppingUser->member->getFullName().'</a>';
|
||||
}
|
||||
|
||||
return '';
|
||||
|
|
@ -202,8 +237,8 @@ class CustomerController extends Controller
|
|||
return get_active_badge($ShoppingUser->subscribed);
|
||||
})
|
||||
->filterColumn('billing_email', function ($query, $keyword) {
|
||||
if ($keyword != "") {
|
||||
$query->where('billing_email', 'LIKE', '%' . $keyword . '%');
|
||||
if ($keyword != '') {
|
||||
$query->where('billing_email', 'LIKE', '%'.$keyword.'%');
|
||||
}
|
||||
})
|
||||
->orderColumn('id', 'id $1')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DcFile;
|
||||
use App\Models\File;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserCredit;
|
||||
use App\Models\UserInvoice;
|
||||
use App\Repositories\CreditRepository;
|
||||
use Auth;
|
||||
use Response;
|
||||
|
|
@ -78,7 +82,7 @@ class FileController extends Controller
|
|||
}
|
||||
}*/
|
||||
if ($from === 'invoice') {
|
||||
$shopping_order = \App\Models\ShoppingOrder::findOrFail($id);
|
||||
$shopping_order = ShoppingOrder::findOrFail($id);
|
||||
if ($shopping_order->user_invoice) {
|
||||
$this->isPermissionShoppingOrder($shopping_order);
|
||||
$user_invoice = $shopping_order->user_invoice;
|
||||
|
|
@ -95,7 +99,7 @@ class FileController extends Controller
|
|||
}
|
||||
|
||||
if ($from === 'delivery') {
|
||||
$shopping_order = \App\Models\ShoppingOrder::findOrFail($id);
|
||||
$shopping_order = ShoppingOrder::findOrFail($id);
|
||||
if ($shopping_order->user_invoice) {
|
||||
$this->isPermissionShoppingOrder($shopping_order);
|
||||
$user_invoice = $shopping_order->user_invoice;
|
||||
|
|
@ -121,12 +125,12 @@ class FileController extends Controller
|
|||
}
|
||||
|
||||
if ($from === 'cancellation') {
|
||||
$shopping_order = \App\Models\ShoppingOrder::findOrFail($id);
|
||||
$shopping_order = ShoppingOrder::findOrFail($id);
|
||||
$this->isPermissionShoppingOrder($shopping_order);
|
||||
|
||||
// Stornorechnung finden: cancellation=true UND cancellation_id=null
|
||||
// (Die Original-Rechnung hat auch cancellation=true, aber MIT cancellation_id)
|
||||
$cancellation_invoice = \App\Models\UserInvoice::where('shopping_order_id', $shopping_order->id)
|
||||
$cancellation_invoice = UserInvoice::where('shopping_order_id', $shopping_order->id)
|
||||
->where('cancellation', true)
|
||||
->whereNull('cancellation_id')
|
||||
->first();
|
||||
|
|
@ -147,7 +151,7 @@ class FileController extends Controller
|
|||
}
|
||||
|
||||
if ($from === 'credit') {
|
||||
$user_credit = \App\Models\UserCredit::findOrFail($id);
|
||||
$user_credit = UserCredit::findOrFail($id);
|
||||
$this->isPermissionUserCredit($user_credit);
|
||||
$disk = $user_credit->disk;
|
||||
// Lokalisierte Version wenn angegeben
|
||||
|
|
@ -161,7 +165,7 @@ class FileController extends Controller
|
|||
}
|
||||
|
||||
if ($from === 'credit_detail') {
|
||||
$user_credit = \App\Models\UserCredit::findOrFail($id);
|
||||
$user_credit = UserCredit::findOrFail($id);
|
||||
$this->isPermissionUserCredit($user_credit);
|
||||
|
||||
return $this->create_credit_detail($user_credit, $do);
|
||||
|
|
@ -175,14 +179,14 @@ class FileController extends Controller
|
|||
|
||||
if ($from === 'dc_file') {
|
||||
// $this->isPermissionAuth();
|
||||
$dc_file = \App\Models\DcFile::findOrFail($id);
|
||||
$dc_file = DcFile::findOrFail($id);
|
||||
$filename = $dc_file->filename;
|
||||
$disk = 'public';
|
||||
$path = $dc_file->getFile();
|
||||
}
|
||||
if ($from === 'dc_thumb') {
|
||||
// $this->isPermissionAuth();
|
||||
$dc_file = \App\Models\DcFile::findOrFail($id);
|
||||
$dc_file = DcFile::findOrFail($id);
|
||||
$filename = $dc_file->filename;
|
||||
$disk = 'public';
|
||||
$path = $dc_file->getThumb();
|
||||
|
|
@ -190,14 +194,14 @@ class FileController extends Controller
|
|||
|
||||
if ($from === 'dc_big') {
|
||||
// $this->isPermissionAuth();
|
||||
$dc_file = \App\Models\DcFile::findOrFail($id);
|
||||
$dc_file = DcFile::findOrFail($id);
|
||||
$filename = $dc_file->filename;
|
||||
$disk = 'public';
|
||||
$path = $dc_file->getBig();
|
||||
}
|
||||
|
||||
if ($from === 'user') {
|
||||
$file = \App\Models\File::findOrFail($id);
|
||||
$file = File::findOrFail($id);
|
||||
$filename = $file->filename;
|
||||
$disk = 'user';
|
||||
$path = $file->dir.$file->filename;
|
||||
|
|
@ -244,6 +248,7 @@ class FileController extends Controller
|
|||
|
||||
private function create_credit_detail(UserCredit $user_credit, $do)
|
||||
{
|
||||
abort_if($user_credit->user === null, 404, 'User zur Gutschrift nicht gefunden.');
|
||||
|
||||
$credit_repo = new CreditRepository($user_credit->user);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ use App\Models\Incentive;
|
|||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Services\Abo\PaylineForecastService;
|
||||
use App\Services\Abo\TeamAboOverviewService;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\Services\BusinessPlan\TreeCalcBot;
|
||||
use App\Services\BusinessPlan\LiveCommissionService;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
use Request;
|
||||
use Util;
|
||||
|
||||
|
|
@ -38,11 +38,11 @@ class HomeController extends Controller
|
|||
return redirect('home');
|
||||
}
|
||||
|
||||
public function newsArchive(): \Illuminate\View\View
|
||||
public function newsArchive(): View
|
||||
{
|
||||
return view('dashboard.news_archive', [
|
||||
'currentNews' => \App\Models\DashboardNews::getActiveNews(),
|
||||
'archiveNews' => \App\Models\DashboardNews::getArchiveNews(),
|
||||
'currentNews' => DashboardNews::getActiveNews(),
|
||||
'archiveNews' => DashboardNews::getArchiveNews(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +111,18 @@ class HomeController extends Controller
|
|||
private function buildMonthlyStats(User $user): array
|
||||
{
|
||||
$month = max(1, min(12, (int) Request::get('stats_month', now()->month)));
|
||||
$year = (int) Request::get('stats_year', now()->year);
|
||||
$year = max(2020, min((int) now()->year + 1, (int) Request::get('stats_year', now()->year)));
|
||||
|
||||
$salesVolume = $user->getUserSalesVolume($month, $year, 'first');
|
||||
[$paylinePoints, $paylineIsLive] = $this->teamPaylinePoints($user, $month, $year);
|
||||
$customerPoints = $salesVolume ? (float) $salesVolume->getPointsKPSum() : 0.0;
|
||||
|
||||
// Payline-Prognose (B10) nur für den laufenden Monat — Basis ist der heutige
|
||||
// Stand aus Kundenumsatz + Payline-Punkten.
|
||||
$isCurrentMonth = $month === (int) now()->month && $year === (int) now()->year;
|
||||
$paylineForecast = $isCurrentMonth
|
||||
? app(PaylineForecastService::class)->forecastDays($user, $customerPoints + (float) $paylinePoints)
|
||||
: null;
|
||||
|
||||
$firstlineIds = User::where('m_sponsor', $user->id)
|
||||
->whereColumn('id', '!=', 'm_sponsor')
|
||||
|
|
@ -130,9 +138,11 @@ class HomeController extends Controller
|
|||
[
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'customer_points' => $salesVolume ? (float) $salesVolume->getPointsKPSum() : 0.0,
|
||||
'customer_points' => $customerPoints,
|
||||
'team_payline_points' => $paylinePoints,
|
||||
'payline_is_live' => $paylineIsLive,
|
||||
'payline_forecast' => $paylineForecast,
|
||||
'commission' => app(LiveCommissionService::class)->commission($user, $month, $year),
|
||||
'direct_new_partners' => $dashboardService->newPartnersCount($firstlineIds, $month, $year),
|
||||
],
|
||||
$teamOverview->teamKpis($user, $month, $year),
|
||||
|
|
@ -141,45 +151,16 @@ class HomeController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Team-Payline-Punkte: aus dem UserBusiness-Snapshot (abgeschlossene Monate) oder
|
||||
* – für den laufenden Monat – einmalig live berechnet und für Folge-Aufrufe gecacht.
|
||||
* Team-Payline-Punkte: aus dem gemeinsamen Live-Lauf ({@see LiveCommissionService}),
|
||||
* der auch die Provision und die Payline-Tiefe liefert — ein Baum-Lauf für alles.
|
||||
*
|
||||
* @return array{0: float, 1: bool} [Punkte, istLive]
|
||||
*/
|
||||
private function teamPaylinePoints(User $user, int $month, int $year): array
|
||||
{
|
||||
$userBusiness = UserBusiness::where('user_id', $user->id)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
$snapshot = app(LiveCommissionService::class)->snapshot($user, $month, $year);
|
||||
|
||||
if ($userBusiness) {
|
||||
return [(float) ($userBusiness->payline_points ?? 0), false];
|
||||
}
|
||||
|
||||
$points = Cache::remember(
|
||||
"home_payline_{$user->id}_{$month}_{$year}",
|
||||
now()->addHours(6),
|
||||
fn (): float => $this->calculatePaylineLive($user, $month, $year)
|
||||
);
|
||||
|
||||
return [(float) $points, true];
|
||||
}
|
||||
|
||||
private function calculatePaylineLive(User $user, int $month, int $year): float
|
||||
{
|
||||
try {
|
||||
$treeCalcBot = new TreeCalcBot($month, $year, 'member');
|
||||
$treeCalcBot->initBusinesslUserDetail($user);
|
||||
|
||||
if ($treeCalcBot->business_user) {
|
||||
return (float) ($treeCalcBot->business_user->payline_points ?? 0);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning("Live-Berechnung Payline-Punkte fehlgeschlagen für User {$user->id}: ".$e->getMessage());
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
return [$snapshot['payline_points'], $snapshot['is_live']];
|
||||
}
|
||||
|
||||
public function loadingModal()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Homeparty;
|
||||
use App\Models\HomepartyUser;
|
||||
use App\Models\Product;
|
||||
|
|
@ -11,10 +12,12 @@ use App\Models\UserAbo;
|
|||
use App\Models\UserCredit;
|
||||
use App\Models\UserLevel;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Abo\TeamAboOverviewService;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\BusinessPlan\TreeCalcBot;
|
||||
use App\Services\BusinessPlan\TreeCalcBotOptimized;
|
||||
use App\Services\DhlModalService;
|
||||
use App\Services\DhlProductResolver;
|
||||
use App\User;
|
||||
use Request;
|
||||
|
||||
|
|
@ -141,7 +144,7 @@ class ModalController extends Controller
|
|||
}
|
||||
|
||||
if ($data['action'] === 'team-abo-member-show') {
|
||||
$summary = app(\App\Services\Abo\TeamAboOverviewService::class)
|
||||
$summary = app(TeamAboOverviewService::class)
|
||||
->memberAboSummary(\Auth::user(), (int) $data['id']);
|
||||
if ($summary) {
|
||||
$ret = view('user.team.modal_member_abos', compact('summary', 'data'))->render();
|
||||
|
|
@ -174,7 +177,7 @@ class ModalController extends Controller
|
|||
$ret = view('admin.payment.modal_add_credit', compact('value', 'data'))->render();
|
||||
}
|
||||
if ($data['action'] === 'user-credit-status') {
|
||||
$UserCredit = UserCredit::find($data['id']); // current user form order
|
||||
$UserCredit = UserCredit::findOrFail($data['id']); // current user form order
|
||||
$ret = view('admin.payment.modal_credit_status', compact('UserCredit', 'data'))->render();
|
||||
}
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
|
|
@ -279,12 +282,12 @@ class ModalController extends Controller
|
|||
'order' => null,
|
||||
'orderWeight' => 1.0,
|
||||
'shippingAddress' => null,
|
||||
'availableCountries' => \App\Models\Country::where('active', 1)->get(),
|
||||
'availableCountries' => Country::where('active', 1)->get(),
|
||||
'productCodes' => [
|
||||
'V01PAK' => 'DHL Paket (National)',
|
||||
'V53WPAK' => 'DHL Paket International',
|
||||
],
|
||||
'productSuggestions' => (new \App\Services\DhlProductResolver)->getProductSuggestionsByCountry(),
|
||||
'productSuggestions' => (new DhlProductResolver)->getProductSuggestionsByCountry(),
|
||||
'selectedProductCode' => 'V01PAK',
|
||||
'errors' => ['Fehler beim Laden der Daten: '.$e->getMessage()],
|
||||
'warnings' => [],
|
||||
|
|
|
|||
|
|
@ -57,8 +57,12 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
$shopping_user = ShoppingUser::findOrFail($user->shopping_user_id);
|
||||
// Gekündigte/beendete Abos (Status 4/5) zählen nicht mehr als "mein Abo" —
|
||||
// nach einer Kündigung muss das Portal wieder die Neuanlage anbieten (Ein-Abo-Regel, A2).
|
||||
$user_abo = UserAbo::where('email', $shopping_user->billing_email)
|
||||
->where('status', '>=', 1)
|
||||
->whereNotIn('status', AboHelper::ENDED_STATUSES)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $user_abo) {
|
||||
|
|
@ -110,6 +114,7 @@ class AboController extends Controller
|
|||
if ($data['action'] === 'abo_update_settings') {
|
||||
$user_abo = UserAbo::findOrFail($data['id']);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
$data['view'] = 'portal';
|
||||
$aboRepository = new AboRepository;
|
||||
$aboRepository->setModel($user_abo);
|
||||
$aboRepository->update($data);
|
||||
|
|
@ -243,6 +248,7 @@ class AboController extends Controller
|
|||
'summary' => $summary,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
'change_locked' => $changeLocked,
|
||||
'portal_view' => true,
|
||||
])->render();
|
||||
$html_comp = view('user.order.comp_product', array_merge($data, [
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
|
|
@ -250,6 +256,7 @@ class AboController extends Controller
|
|||
]))->render();
|
||||
$html_summary = view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
'portal_view' => true,
|
||||
])->render();
|
||||
|
||||
$amount = $user_abo->getFormattedAmount();
|
||||
|
|
@ -409,9 +416,11 @@ class AboController extends Controller
|
|||
'only_show_products' => false,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
'change_locked' => $changeLocked,
|
||||
'portal_view' => true,
|
||||
])->render(),
|
||||
'html_summary' => view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
'portal_view' => true,
|
||||
])->render(),
|
||||
'html_comp' => view('user.order.comp_product', [
|
||||
'comp_products' => [],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use App\Services\SyS\CleanHTMLProductDescription;
|
|||
use App\Services\SyS\Correction;
|
||||
use App\Services\SyS\Cronjobs;
|
||||
use App\Services\SyS\Customers;
|
||||
use App\Services\SyS\DomainSSL;
|
||||
use App\Services\SyS\Import;
|
||||
use App\Services\SyS\ImportDbipCountry;
|
||||
use App\Services\SyS\PayoneCallbackTestbench;
|
||||
|
|
@ -57,9 +56,6 @@ class SysController extends Controller
|
|||
case 'cronjobs':
|
||||
return Cronjobs::show();
|
||||
break;
|
||||
case 'domainssl':
|
||||
return DomainSSL::show();
|
||||
break;
|
||||
case 'shopping_orders':
|
||||
return ShoppingOrders::show();
|
||||
break;
|
||||
|
|
@ -115,9 +111,6 @@ class SysController extends Controller
|
|||
case 'cronjobs':
|
||||
return Cronjobs::store();
|
||||
break;
|
||||
case 'domainssl':
|
||||
return DomainSSL::store();
|
||||
break;
|
||||
case 'shopping_orders':
|
||||
return ShoppingOrders::store();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -158,7 +158,13 @@ class AboController extends Controller
|
|||
|
||||
if (isset($data['action'])) {
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
// Body-id kann von der Route abweichen: Berechtigungen erneut auf dem
|
||||
// tatsächlich bearbeiteten Abo prüfen. Die Sicht kommt ausschließlich aus
|
||||
// der (bereits geprüften) Route, nie aus dem Formular — sonst ließen sich
|
||||
// per view=admin sämtliche Selfservice-Regeln umgehen.
|
||||
$user_abo = UserAbo::findOrFail($data['id']);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
$data['view'] = $view;
|
||||
$this->aboRepository->setModel($user_abo);
|
||||
$this->aboRepository->update($data);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\Services\Backoffice\BackofficeDrilldownService;
|
||||
use App\Services\Backoffice\PersonalStatisticsService;
|
||||
use App\Services\BusinessPlan\LiveCommissionService;
|
||||
use App\Services\HTMLHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
|
@ -47,6 +48,7 @@ class BackofficeStatisticsController extends Controller
|
|||
'filterYears' => HTMLHelper::getYearRange(2022),
|
||||
'statistics' => $statistics,
|
||||
'performance' => $performance,
|
||||
'commission' => app(LiveCommissionService::class)->commission($request->user(), $selectedMonth, $selectedYear),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -76,11 +78,12 @@ class BackofficeStatisticsController extends Controller
|
|||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$line = (int) $request->get('line', 1);
|
||||
$metric = (string) $request->get('metric', 'consultants');
|
||||
$onlyNew = $request->boolean('only_new');
|
||||
|
||||
return view('user.backoffice.statistics.details', [
|
||||
'selectedMonth' => $selectedMonth,
|
||||
'selectedYear' => $selectedYear,
|
||||
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear),
|
||||
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -93,10 +96,12 @@ class BackofficeStatisticsController extends Controller
|
|||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$line = (int) $request->get('line', 1);
|
||||
$metric = (string) $request->get('metric', 'consultants');
|
||||
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear);
|
||||
$onlyNew = $request->boolean('only_new');
|
||||
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew);
|
||||
$filename = sprintf(
|
||||
'backoffice-statistik-%s-linie-%s-%02d-%d.csv',
|
||||
'backoffice-statistik-%s%s-linie-%s-%02d-%d.csv',
|
||||
$metric,
|
||||
$details['only_new'] ? '-neu' : '',
|
||||
$line === 0 ? 'alle' : $line,
|
||||
$selectedMonth,
|
||||
$selectedYear
|
||||
|
|
@ -156,7 +161,7 @@ class BackofficeStatisticsController extends Controller
|
|||
private function selectedPeriod(Request $request): array
|
||||
{
|
||||
$selectedMonth = max(1, min(12, (int) $request->get('month', session(self::SESSION_MONTH_KEY, now()->month))));
|
||||
$selectedYear = (int) $request->get('year', session(self::SESSION_YEAR_KEY, now()->year));
|
||||
$selectedYear = max(2020, min((int) now()->year + 1, (int) $request->get('year', session(self::SESSION_YEAR_KEY, now()->year))));
|
||||
|
||||
session([
|
||||
self::SESSION_MONTH_KEY => $selectedMonth,
|
||||
|
|
@ -232,6 +237,10 @@ class BackofficeStatisticsController extends Controller
|
|||
return ['Name', 'E-Mail', 'Karriere-Level', 'Eigenpunkte', 'Shop-Punkte', 'Abo-Punkte', 'Sonstige Punkte', 'Kundenpunkte', 'Gesamtpunkte'];
|
||||
}
|
||||
|
||||
if ($metric === 'new_partners') {
|
||||
return ['Name', 'E-Mail', 'Karriere-Level', 'Sponsor', 'Kundenpunkte', 'Aktiv seit', 'Account gueltig bis'];
|
||||
}
|
||||
|
||||
return ['Name', 'E-Mail', 'Karriere-Level', 'Aktiv seit', 'Account gueltig bis', 'Account Status'];
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +289,18 @@ class BackofficeStatisticsController extends Controller
|
|||
];
|
||||
}
|
||||
|
||||
if ($metric === 'new_partners') {
|
||||
return [
|
||||
$row['name'] ?? '',
|
||||
$row['email'] ?? '',
|
||||
$row['career_level'] ?? '',
|
||||
$row['sponsor'] ?? '',
|
||||
$row['customer_points'] ?? 0,
|
||||
$row['active_date'] ?? '',
|
||||
$row['payment_account'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
$row['name'] ?? '',
|
||||
$row['email'] ?? '',
|
||||
|
|
@ -311,6 +332,10 @@ class BackofficeStatisticsController extends Controller
|
|||
return ['Summe', $summary['count'].' Eintraege', '', $summary['own_points'], $summary['customer_single_order_points'], $summary['customer_abo_points'], $summary['customer_other_points'], $summary['external_points'], $summary['total_points']];
|
||||
}
|
||||
|
||||
if ($metric === 'new_partners') {
|
||||
return ['Summe', $summary['count'].' Eintraege', '', '', $summary['customer_points'], '', '', ''];
|
||||
}
|
||||
|
||||
return ['Summe', $summary['count'].' Eintraege', '', '', '', ''];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@ namespace App\Http\Controllers\User;
|
|||
|
||||
use App\Exports\UserTeamExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserLevel;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Abo\PaylineForecastService;
|
||||
use App\Services\Abo\TeamAboOverviewService;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\BusinessPlan\ExportBot;
|
||||
use App\Services\BusinessPlan\TreeCalcBot;
|
||||
use App\Services\BusinessPlan\TreeCalcBotOptimized;
|
||||
|
|
@ -16,6 +21,7 @@ use App\Services\LevelReportService;
|
|||
use App\Services\NextLevelBadgeHelper;
|
||||
use App\Services\TranslationHelper;
|
||||
use App\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Request;
|
||||
|
||||
|
|
@ -318,6 +324,9 @@ class TeamController extends Controller
|
|||
})
|
||||
->addColumn('is_qual_kp', function ($teamUser) {
|
||||
$user = User::find($teamUser->user_id);
|
||||
if ($user === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return TreeHelperOptimized::generateQualKPBadgeForUser($user, $this->month, $this->year);
|
||||
})
|
||||
|
|
@ -652,7 +661,7 @@ class TeamController extends Controller
|
|||
$currentLevel = $user->user_level;
|
||||
|
||||
// Lade alle aktiven User Level, sortiert nach Position
|
||||
$userLevels = \App\Models\UserLevel::where('active', true)
|
||||
$userLevels = UserLevel::where('active', true)
|
||||
->orderBy('pos', 'asc')
|
||||
->get();
|
||||
|
||||
|
|
@ -706,7 +715,7 @@ class TeamController extends Controller
|
|||
* Konsolidierte Team-Abo-Übersicht: Abos im Team nach Ebenen/Linien gruppiert,
|
||||
* inkl. Eigen-Abos, Kundenabos und neuer Abos im gewählten Monat.
|
||||
*/
|
||||
public function showAbos(\App\Services\Abo\TeamAboOverviewService $overviewService)
|
||||
public function showAbos(TeamAboOverviewService $overviewService, PaylineForecastService $paylineService)
|
||||
{
|
||||
$user = User::find(\Auth::user()->id);
|
||||
$now = now();
|
||||
|
|
@ -725,6 +734,7 @@ class TeamController extends Controller
|
|||
'growthChart' => $this->buildGrowthChart($growth),
|
||||
'growthYear' => $growthYear,
|
||||
'filterYears' => HTMLHelper::getYearRange(2022),
|
||||
'paylineAboTurnover' => $paylineService->aboTurnover($user, (int) $now->month, (int) $now->year),
|
||||
];
|
||||
|
||||
return view('user.team.abos', $data);
|
||||
|
|
@ -767,7 +777,7 @@ class TeamController extends Controller
|
|||
* (`user_abos_team_show`) eingeflossen. Diese Route bleibt für Bookmarks/Altlinks
|
||||
* bestehen und leitet dauerhaft dorthin weiter.
|
||||
*/
|
||||
public function showTeamCustomerAbos(): \Illuminate\Http\RedirectResponse
|
||||
public function showTeamCustomerAbos(): RedirectResponse
|
||||
{
|
||||
return redirect()->route('user_abos_team_show');
|
||||
}
|
||||
|
|
@ -778,7 +788,7 @@ class TeamController extends Controller
|
|||
public function detailAbo($id)
|
||||
{
|
||||
$user = User::find(\Auth::user()->id);
|
||||
$user_abo = \App\Models\UserAbo::findOrFail($id);
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
|
||||
// Prüfe ob das Abo zu einem Team-Mitglied gehört (effizient via Sponsor-Kette)
|
||||
if (! AboHelper::isUserInTeam($user->id, $user_abo->user_id)) {
|
||||
|
|
@ -786,9 +796,9 @@ class TeamController extends Controller
|
|||
}
|
||||
|
||||
// Lade Abo-Details (ähnlich wie AboController)
|
||||
\App\Services\AboOrderCart::initYard($user_abo);
|
||||
$customer_detail = \App\Services\AboOrderCart::getCustomerDetail();
|
||||
\App\Services\AboOrderCart::makeOrderYard($user_abo);
|
||||
AboOrderCart::initYard($user_abo);
|
||||
$customer_detail = AboOrderCart::getCustomerDetail();
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
|
||||
$data = [
|
||||
'user_abo' => $user_abo,
|
||||
|
|
@ -1071,7 +1081,7 @@ class TeamController extends Controller
|
|||
{
|
||||
$levels = [0 => 'Alle Level'];
|
||||
|
||||
$userLevels = \App\Models\UserLevel::orderBy('pos')->get(['id', 'name']);
|
||||
$userLevels = UserLevel::orderBy('pos')->get(['id', 'name']);
|
||||
foreach ($userLevels as $level) {
|
||||
$levels[$level->id] = $level->name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Models\UserShop;
|
||||
use App\Models\UserShopOnSite;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\Slim;
|
||||
use Auth;
|
||||
use Cviebrock\EloquentSluggable\Services\SlugService;
|
||||
use Request;
|
||||
use Response;
|
||||
use Validator;
|
||||
|
||||
|
||||
class UserShopController extends Controller
|
||||
{
|
||||
protected $db;
|
||||
|
||||
protected $userRepo;
|
||||
|
||||
public function __construct(UserRepository $userRepo)
|
||||
|
|
@ -28,9 +27,9 @@ class UserShopController extends Controller
|
|||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
if ($user->shop && !$user->shop->set_defaults) {
|
||||
if ($user->shop && ! $user->shop->set_defaults) {
|
||||
if ($user->account) {
|
||||
$user->shop->title = $user->account->first_name . " " . $user->account->last_name;
|
||||
$user->shop->title = $user->account->first_name.' '.$user->account->last_name;
|
||||
}
|
||||
if ($user->account) {
|
||||
$user->shop->contact = $this->generate_contact($user);
|
||||
|
|
@ -45,6 +44,7 @@ class UserShopController extends Controller
|
|||
$data = [
|
||||
'user' => $user,
|
||||
];
|
||||
|
||||
return view('user.shop', $data);
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +56,7 @@ class UserShopController extends Controller
|
|||
'localizations' => array_keys(config('localization.supportedLocales')),
|
||||
|
||||
];
|
||||
|
||||
return view('user.shop.translate', $data);
|
||||
}
|
||||
|
||||
|
|
@ -64,13 +65,14 @@ class UserShopController extends Controller
|
|||
$user = Auth::user();
|
||||
$data = Request::all();
|
||||
|
||||
if (!$user->shop) {
|
||||
if (! $user->shop) {
|
||||
abort(404);
|
||||
}
|
||||
foreach ($data['trans'] as $lang => $val) {
|
||||
$this->storeTranslations($user->shop, $lang, $val);
|
||||
}
|
||||
\Session()->flash('alert-save', true);
|
||||
|
||||
return redirect(route('user_shop_translate'));
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +81,7 @@ class UserShopController extends Controller
|
|||
$user = Auth::user();
|
||||
$data = Request::all();
|
||||
|
||||
if (!$user->shop) {
|
||||
if (! $user->shop) {
|
||||
abort(404);
|
||||
}
|
||||
$user->shop->title = $data['title'];
|
||||
|
|
@ -100,34 +102,35 @@ class UserShopController extends Controller
|
|||
$user_shop->accessibility = trim(preg_replace('/\s*\n+/', "\n", $data['accessibility']));
|
||||
$user_shop->about = trim(preg_replace('/\s+/', ' ', $data['about']));
|
||||
$user_shop->save();
|
||||
|
||||
return;
|
||||
}
|
||||
$trans = $user_shop->trans;
|
||||
$trans[$lang]['contact'] = trim(preg_replace('/\s*\n+/', "\n", $data['contact']));
|
||||
$trans[$lang]['accessibility'] = trim(preg_replace('/\s*\n+/', "\n", $data['accessibility']));
|
||||
$trans[$lang]['about'] = trim(preg_replace('/\s+/', ' ', $data['about']));
|
||||
$trans[$lang]['about'] = trim(preg_replace('/\s+/', ' ', $data['about']));
|
||||
$user_shop->trans = $trans;
|
||||
$user_shop->save();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
private function generate_contact($user)
|
||||
{
|
||||
$ret = "";
|
||||
$ret = '';
|
||||
$sep = "\n";
|
||||
|
||||
$ret = $user->account->street != "" ? $user->account->street : __('shop.your_street_number');
|
||||
$ret .= " • ";
|
||||
$ret .= $user->account->postal_code != "" ? $user->account->postal_code . " " : __('shop.your_zip_code');
|
||||
$ret .= $user->account->city != "" ? $user->account->city : __('shop.your_city');
|
||||
$ret = $user->account->street != '' ? $user->account->street : __('shop.your_street_number');
|
||||
$ret .= ' • ';
|
||||
$ret .= $user->account->postal_code != '' ? $user->account->postal_code.' ' : __('shop.your_zip_code');
|
||||
$ret .= $user->account->city != '' ? $user->account->city : __('shop.your_city');
|
||||
$ret .= $sep;
|
||||
|
||||
$pre = $user->account->pre_phone_id != "" ? $user->account->pre_phone->phone . " " : "";
|
||||
$ret .= __('shop.phone') . ": " . ($user->account->phone != "" ? $pre . $user->account->phone : __('shop.your_phone_number'));
|
||||
$pre = $user->account->pre_phone_id != '' ? $user->account->pre_phone->phone.' ' : '';
|
||||
$ret .= __('shop.phone').': '.($user->account->phone != '' ? $pre.$user->account->phone : __('shop.your_phone_number'));
|
||||
$ret .= $sep;
|
||||
|
||||
$pre = $user->account->pre_mobil_id != "" ? $user->account->pre_mobil->phone . " " : "";
|
||||
$ret .= __('shop.mobil') . ": " . ($user->account->mobil != "" ? $pre . $user->account->mobil : __('shop.your_mobile_number'));
|
||||
$pre = $user->account->pre_mobil_id != '' ? $user->account->pre_mobil->phone.' ' : '';
|
||||
$ret .= __('shop.mobil').': '.($user->account->mobil != '' ? $pre.$user->account->mobil : __('shop.your_mobile_number'));
|
||||
$ret .= $sep;
|
||||
|
||||
$ret .= $user->email;
|
||||
|
|
@ -142,31 +145,32 @@ class UserShopController extends Controller
|
|||
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user->shop) {
|
||||
if (! $user->shop) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = \App\Services\Slim::getImages('images')[0];
|
||||
$image = Slim::getImages('images')[0];
|
||||
|
||||
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']])) {
|
||||
if (! isset($file_ex[$image['output']['type']])) {
|
||||
\Session()->flash('alert-danger', 'File is not jpg or png!');
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
}
|
||||
|
||||
$ext = $file_ex[$image['output']['type']];
|
||||
// Original file name
|
||||
$name = $image['output']['name'];
|
||||
$name = \App\Services\Slim::sanitizeFileName($name);
|
||||
$name = uniqid() . '_' . $name;
|
||||
$name = Slim::sanitizeFileName($name);
|
||||
$name = uniqid().'_'.$name;
|
||||
|
||||
$data = \Storage::disk('public')->put(
|
||||
'images/shop/' . $name,
|
||||
'images/shop/'.$name,
|
||||
$data
|
||||
);
|
||||
|
||||
|
|
@ -177,15 +181,16 @@ class UserShopController extends Controller
|
|||
$user->shop->size = $image['input']['size'];
|
||||
$user->shop->save();
|
||||
|
||||
|
||||
|
||||
\Session()->flash('alert-success', __('msg.file_uploaded'));
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
}
|
||||
\Session()->flash('alert-danger', __('msg.file_empty'));
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
} catch (\Exception $e) {
|
||||
\Session()->flash('alert-danger', "Error: " . $e);
|
||||
\Session()->flash('alert-danger', 'Error: '.$e);
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
}
|
||||
}
|
||||
|
|
@ -195,12 +200,12 @@ class UserShopController extends Controller
|
|||
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user->shop) {
|
||||
if (! $user->shop) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($user->shop->filename) {
|
||||
$file = 'images/shop/' . $user->shop->filename;
|
||||
$file = 'images/shop/'.$user->shop->filename;
|
||||
\Storage::disk('public')->delete($file);
|
||||
|
||||
$user->shop->filename = null;
|
||||
|
|
@ -211,9 +216,11 @@ class UserShopController extends Controller
|
|||
$user->shop->save();
|
||||
|
||||
\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'));
|
||||
}
|
||||
|
||||
|
|
@ -223,32 +230,33 @@ class UserShopController extends Controller
|
|||
$user = Auth::user();
|
||||
$user_shop_id = Request::get('user_shop_id');
|
||||
|
||||
if (!$user->shop || $user->shop->id != $user_shop_id) {
|
||||
if (! $user->shop || $user->shop->id != $user_shop_id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = \App\Services\Slim::getImages('images')[0];
|
||||
$image = Slim::getImages('images')[0];
|
||||
|
||||
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']])) {
|
||||
if (! isset($file_ex[$image['output']['type']])) {
|
||||
\Session()->flash('alert-danger', 'File is not jpg or png!');
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
}
|
||||
|
||||
$ext = $file_ex[$image['output']['type']];
|
||||
// Original file name
|
||||
$name = $image['output']['name'];
|
||||
$name = \App\Services\Slim::sanitizeFileName($name);
|
||||
$name = uniqid() . '_' . $name;
|
||||
$name = Slim::sanitizeFileName($name);
|
||||
$name = uniqid().'_'.$name;
|
||||
|
||||
$data = \Storage::disk('public')->put(
|
||||
'images/user_shop/' . $user->shop->id . '/' . $name,
|
||||
'images/user_shop/'.$user->shop->id.'/'.$name,
|
||||
$data
|
||||
);
|
||||
|
||||
|
|
@ -258,16 +266,19 @@ class UserShopController 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('user_shop'));
|
||||
}
|
||||
\Session()->flash('alert-danger', __('msg.file_empty'));
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
} catch (\Exception $e) {
|
||||
\Session()->flash('alert-danger', "Error: " . $e);
|
||||
\Session()->flash('alert-danger', 'Error: '.$e);
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
}
|
||||
}
|
||||
|
|
@ -276,74 +287,78 @@ class UserShopController extends Controller
|
|||
{
|
||||
|
||||
$user = Auth::user();
|
||||
if (!$user->shop || $user->shop->id != $user_shop_id) {
|
||||
if (! $user->shop || $user->shop->id != $user_shop_id) {
|
||||
abort(404);
|
||||
}
|
||||
$image = UserShopOnSite::findOrFail($image_id);
|
||||
|
||||
if ($image->user_shop_id == $user_shop_id) {
|
||||
$file = 'images/user_shop/' . $user_shop_id . '/' . $image->filename;
|
||||
$file = 'images/user_shop/'.$user_shop_id.'/'.$image->filename;
|
||||
\Storage::disk('public')->delete($file);
|
||||
|
||||
$image->delete();
|
||||
|
||||
\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()
|
||||
{
|
||||
|
||||
if (Request::get('shop_submit') == 'check') {
|
||||
$rules = array(
|
||||
$rules = [
|
||||
'user_shop_name' => ' required|alpha_dash|unique:user_shops,name|min:4|max:20|full_word_check',
|
||||
);
|
||||
];
|
||||
Validator::extend('full_word_check', function ($attribute, $value, $parameters, $validator) {
|
||||
if (in_array($value, config('profanity.full_word_check'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$validator = Validator::make(Request::all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
\Session()->flash('shop-name-error', 'error');
|
||||
|
||||
return redirect()->back()->withErrors($validator)->withInput(Request::all());
|
||||
}
|
||||
\Session()->flash('shop-name-error', 'check');
|
||||
if (Request::get('user_shop_id')) {
|
||||
return back()->withInput(Request::all());
|
||||
}
|
||||
|
||||
return redirect(route('user_shop'))->withInput(Request::all());
|
||||
}
|
||||
|
||||
if (Request::get('shop_submit') == 'action') {
|
||||
|
||||
$rules = array(
|
||||
$rules = [
|
||||
'user_shop_name' => ' required|alpha_dash|unique:user_shops,name|min:4|max:20|full_word_check',
|
||||
'user_shop_active' => 'accepted',
|
||||
|
||||
);
|
||||
];
|
||||
Validator::extend('full_word_check', function ($attribute, $value, $parameters, $validator) {
|
||||
if (in_array($value, config('profanity.full_word_check'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$validator = Validator::make(Request::all(), $rules);
|
||||
if ($validator->fails()) {
|
||||
\Session()->flash('shop-name-error', 'error');
|
||||
|
||||
return redirect()->back()->withErrors($validator)->withInput(Request::all());
|
||||
}
|
||||
\Session()->flash('shop-name-error', 'check');
|
||||
|
||||
//all is right - save
|
||||
// all is right - save
|
||||
$user = Auth::user();
|
||||
$data = Request::all();
|
||||
$slug = SlugService::createSlug(UserShop::class, 'slug', $data['user_shop_name']);
|
||||
|
|
@ -367,93 +382,51 @@ class UserShopController extends Controller
|
|||
);
|
||||
}
|
||||
\Session()->flash('alert-save', true);
|
||||
|
||||
return redirect(route('user_shop'));
|
||||
/*$ret = $this->userShopRegisterSubDomain($user_shop->slug);
|
||||
if($ret['success'] === true){
|
||||
\Session()->flash('alert-save', true);
|
||||
}else{
|
||||
$user_shop->forceDelete();
|
||||
\Session()->flash('alert-error', $ret['error']);
|
||||
}
|
||||
return redirect(route('user_shop'));*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function userShopRegisterSubDomain($slug)
|
||||
{
|
||||
|
||||
$kas = new KasController();
|
||||
$domain = 'mivita.care';
|
||||
|
||||
|
||||
//check if exisist
|
||||
$subdomains = $kas->action('get_subdomains');
|
||||
foreach ($subdomains as $subdomain) {
|
||||
if (!isset($subdomain['subdomain_name'])) {
|
||||
continue;
|
||||
}
|
||||
$sub = str_replace("." . $domain, '', $subdomain['subdomain_name']);
|
||||
if ($sub == $slug) {
|
||||
return ['success' => false, 'error' => __('shop.error_subdomain_exists')];
|
||||
}
|
||||
}
|
||||
//add
|
||||
$full_subdomain_name = $slug . "." . $domain;
|
||||
$pra = array(
|
||||
'subdomain_name' => $slug,
|
||||
'domain_name' => $domain,
|
||||
'subdomain_path' => '/mein.mivita.care/public/',
|
||||
'php_version' => config('app.php_version'),
|
||||
//'ssl_proxy' => 'Y',
|
||||
//'redirect_status' => 0
|
||||
);
|
||||
$add_subdomain = $kas->action('add_subdomain', $pra);
|
||||
if ($add_subdomain == $full_subdomain_name) {
|
||||
return ['success' => true];
|
||||
}
|
||||
return ['success' => false, 'error' => $add_subdomain];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string to ajax
|
||||
*/
|
||||
public function checkUserShopName()
|
||||
{
|
||||
|
||||
$rules = array(
|
||||
$rules = [
|
||||
'user_shop_name' => ' required|alpha_dash|unique:user_shops,name|min:4|max:20|full_word_check',
|
||||
);
|
||||
];
|
||||
Validator::extend('full_word_check', function ($attribute, $value, $parameters, $validator) {
|
||||
if (in_array($value, config('profanity.full_word_check'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$validator = Validator::make(Request::all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
//$messages = $validator->messages();
|
||||
return Response::json(array(
|
||||
// $messages = $validator->messages();
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'errors' => $validator->getMessageBag()->toArray()
|
||||
'errors' => $validator->getMessageBag()->toArray(),
|
||||
|
||||
));
|
||||
]);
|
||||
}
|
||||
$slug = SlugService::createSlug(UserShop::class, 'slug', Request::get('user_shop_name'));
|
||||
|
||||
return Response::json(array(
|
||||
return Response::json([
|
||||
'success' => true,
|
||||
'preview_user_shop_name' => "https://" . $slug . "." . config('app.domain') . config('app.tld_care'),
|
||||
));
|
||||
'preview_user_shop_name' => 'https://'.$slug.'.'.config('app.domain').config('app.tld_care'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function editName()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$user_shop = $user->shop;
|
||||
if (!$user_shop) {
|
||||
if (! $user_shop) {
|
||||
abort(404);
|
||||
}
|
||||
$user_shop_domain = $user_shop->getSubdomain(false);
|
||||
|
|
@ -462,6 +435,7 @@ class UserShopController extends Controller
|
|||
'user_shop_id' => $user_shop->id,
|
||||
'user_shop_domain' => $user_shop_domain,
|
||||
];
|
||||
|
||||
return view('user.shop_edit_name', $data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,43 @@ use App\Models\Product;
|
|||
use App\Models\ProductCategory;
|
||||
use App\Services\LocaleGuard;
|
||||
use App\Services\Shop;
|
||||
use App\Services\UserService;
|
||||
use App\Services\Util;
|
||||
use Request;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Templates, die eigene Controller mit eigenen View-Daten haben und daher
|
||||
* nicht über die Catch-all-Route ({site}/{subsite?}) gerendert werden dürfen.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const PROTECTED_TEMPLATES = [
|
||||
'checkout',
|
||||
'checkout-error',
|
||||
'checkout-final',
|
||||
'checkout-is-final',
|
||||
'card',
|
||||
'kontakt',
|
||||
'contact-final',
|
||||
'registrierung',
|
||||
'registrierung_finish',
|
||||
'widerruf',
|
||||
'widerruf-final',
|
||||
'produkte-show',
|
||||
'produkte-item',
|
||||
'versandkosten',
|
||||
'zahlungsarten',
|
||||
];
|
||||
|
||||
private function isRenderableTemplate(string $template): bool
|
||||
{
|
||||
return ! str_starts_with($template, '_')
|
||||
&& ! in_array($template, self::PROTECTED_TEMPLATES, true)
|
||||
&& view()->exists('web.templates.'.$template);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->setIPInfo();
|
||||
|
|
@ -83,7 +115,7 @@ class SiteController extends Controller
|
|||
// ist default
|
||||
|
||||
// sprache
|
||||
if (array_key_exists($country, \App\Services\UserService::getTransChange())) {
|
||||
if (array_key_exists($country, UserService::getTransChange())) {
|
||||
\Session::put('user_init_country', $country);
|
||||
\Session::put('locale', $country);
|
||||
\App::setLocale($country);
|
||||
|
|
@ -177,13 +209,13 @@ class SiteController extends Controller
|
|||
'yard_instance' => 'webshop',
|
||||
];
|
||||
if ($subsite) {
|
||||
if (! view()->exists('web.templates.'.$subsite)) {
|
||||
if (! $this->isRenderableTemplate($subsite)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('web.templates.'.$subsite, $data);
|
||||
}
|
||||
if (! view()->exists('web.templates.'.$site)) {
|
||||
if (! $this->isRenderableTemplate($site)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\ShoppingOrder
|
||||
|
|
@ -23,16 +28,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property int|null $weight
|
||||
* @property int|null $paid
|
||||
* @property string|null $txaction
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\User|null $auth_user
|
||||
* @property-read \App\Models\Country $country
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingOrderItem[] $shopping_order_items
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read User|null $auth_user
|
||||
* @property-read Country $country
|
||||
* @property-read Collection|ShoppingOrderItem[] $shopping_order_items
|
||||
* @property-read int|null $shopping_order_items_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingPayment[] $shopping_payments
|
||||
* @property-read Collection|ShoppingPayment[] $shopping_payments
|
||||
* @property-read int|null $shopping_payments_count
|
||||
* @property-read \App\Models\ShoppingUser $shopping_user
|
||||
* @property-read \App\Models\UserShop $user_shop
|
||||
* @property-read ShoppingUser $shopping_user
|
||||
* @property-read UserShop $user_shop
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrder newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrder newQuery()
|
||||
|
|
@ -60,13 +65,13 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
*
|
||||
* @property int|null $member_id
|
||||
* @property string|null $mode
|
||||
* @property-read \App\User|null $member
|
||||
* @property-read \App\Models\UserHistory|null $user_history
|
||||
* @property-read User|null $member
|
||||
* @property-read UserHistory|null $user_history
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrder whereMemberId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrder whereMode($value)
|
||||
*
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property Carbon|null $deleted_at
|
||||
* @property string|null $user_deleted_at
|
||||
*
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrder onlyTrashed()
|
||||
|
|
@ -75,7 +80,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrder withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrder withoutTrashed()
|
||||
*
|
||||
* @property-read \App\Models\ShippingCountry $shipping_country
|
||||
* @property-read ShippingCountry $shipping_country
|
||||
* @property float|null $shipping_net
|
||||
* @property float|null $subtotal_ws
|
||||
* @property int|null $points
|
||||
|
|
@ -93,21 +98,21 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property int|null $homeparty_id
|
||||
* @property array|null $api_notice
|
||||
* @property int|null $api_status
|
||||
* @property-read \App\Models\Homeparty|null $homeparty
|
||||
* @property-read Homeparty|null $homeparty
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereHomepartyId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereWpNotice($value)
|
||||
*
|
||||
* @property array|null $tax_split
|
||||
* @property-read \App\Models\UserInvoice|null $user_invoice
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserSalesVolume[] $user_sales_volume
|
||||
* @property-read UserInvoice|null $user_invoice
|
||||
* @property-read Collection|UserSalesVolume[] $user_sales_volume
|
||||
* @property-read int|null $user_sales_volume_count
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereTaxSplit($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereApiNotice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereApiStatus($value)
|
||||
*
|
||||
* @property-read \App\Models\ShoppingCollectOrder|null $shopping_collect_order
|
||||
* @property-read ShoppingCollectOrder|null $shopping_collect_order
|
||||
* @property array|null $net_split
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereNetSplit($value)
|
||||
|
|
@ -122,11 +127,11 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereAboInterval($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereIsAbo($value)
|
||||
*
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Acme\Dhl\Models\DhlShipment> $dhlOutboundShipments
|
||||
* @property-read Collection<int, DhlShipment> $dhlOutboundShipments
|
||||
* @property-read int|null $dhl_outbound_shipments_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Acme\Dhl\Models\DhlShipment> $dhlReturnShipments
|
||||
* @property-read Collection<int, DhlShipment> $dhlReturnShipments
|
||||
* @property-read int|null $dhl_return_shipments_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Acme\Dhl\Models\DhlShipment> $dhlShipments
|
||||
* @property-read Collection<int, DhlShipment> $dhlShipments
|
||||
* @property-read int|null $dhl_shipments_count
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
|
|
@ -608,7 +613,7 @@ class ShoppingOrder extends Model
|
|||
*/
|
||||
public function isCancellationInvoice()
|
||||
{
|
||||
return \App\Models\UserInvoice::where('shopping_order_id', $this->id)
|
||||
return UserInvoice::where('shopping_order_id', $this->id)
|
||||
->where('cancellation', true)
|
||||
->whereNull('cancellation_id')
|
||||
->exists();
|
||||
|
|
@ -620,7 +625,7 @@ class ShoppingOrder extends Model
|
|||
*/
|
||||
public function getCancellationInvoice()
|
||||
{
|
||||
return \App\Models\UserInvoice::where('shopping_order_id', $this->id)
|
||||
return UserInvoice::where('shopping_order_id', $this->id)
|
||||
->where('cancellation', true)
|
||||
->whereNull('cancellation_id')
|
||||
->first();
|
||||
|
|
@ -710,13 +715,13 @@ class ShoppingOrder extends Model
|
|||
}
|
||||
|
||||
foreach ($tax_split as $key => $value) {
|
||||
$tax_split[$key]['vk_tax'] = number_format($value['vk_tax'], 2);
|
||||
$tax_split[$key]['ek_tax'] = number_format($value['ek_tax'], 2);
|
||||
$tax_split[$key]['vk_tax'] = number_format($value['vk_tax'], 2, '.', '');
|
||||
$tax_split[$key]['ek_tax'] = number_format($value['ek_tax'], 2, '.', '');
|
||||
}
|
||||
|
||||
foreach ($net_split as $key => $value) {
|
||||
$net_split[$key]['vk_net'] = number_format($value['vk_net'], 2);
|
||||
$net_split[$key]['ek_net'] = number_format($value['ek_net'], 2);
|
||||
$net_split[$key]['vk_net'] = number_format($value['vk_net'], 2, '.', '');
|
||||
$net_split[$key]['ek_net'] = number_format($value['ek_net'], 2, '.', '');
|
||||
}
|
||||
}
|
||||
if (! isset($tax_split[16]) && ! isset($tax_split[19])) {
|
||||
|
|
@ -779,10 +784,10 @@ class ShoppingOrder extends Model
|
|||
}
|
||||
|
||||
foreach ($tax_split as $key => $value) {
|
||||
$tax_split[$key] = number_format($value, 2);
|
||||
$tax_split[$key] = number_format($value, 2, '.', '');
|
||||
}
|
||||
foreach ($net_split as $key => $value) {
|
||||
$net_split[$key] = number_format($value, 2);
|
||||
$net_split[$key] = number_format($value, 2, '.', '');
|
||||
}
|
||||
}
|
||||
$this->tax_split = $tax_split;
|
||||
|
|
@ -798,7 +803,7 @@ class ShoppingOrder extends Model
|
|||
$fullname = $this->shopping_user->getFullNameAsArray();
|
||||
$ret = '';
|
||||
$ret .= $fullname['company'] ? $fullname['company'].' | ' : '';
|
||||
$ret .= $fullname['salutation'] ? \App\Services\HTMLHelper::getSalutationLang($fullname['salutation']).' ' : '';
|
||||
$ret .= $fullname['salutation'] ? HTMLHelper::getSalutationLang($fullname['salutation']).' ' : '';
|
||||
$ret .= $fullname['firstname'] ? $fullname['firstname'].' ' : '';
|
||||
$ret .= $fullname['lastname'] ? $fullname['lastname'] : '';
|
||||
$ret .= $fullname['email'] ? ' | '.$fullname['email'].'' : '';
|
||||
|
|
|
|||
|
|
@ -91,6 +91,23 @@ class UserAbo extends Model
|
|||
|
||||
protected $table = 'user_abos';
|
||||
|
||||
/**
|
||||
* Ein gelöschtes Abo darf nicht aktiv bleiben: Softdelete setzt nur
|
||||
* deleted_at, wodurch active=1-Leichen entstehen, die Incentive-,
|
||||
* Statistik- und Ist/Soll-Zahlen auseinanderlaufen lassen. Deshalb wird
|
||||
* active beim Löschen immer mitgesetzt (gilt für Model-Deletes; Query-
|
||||
* Builder-Deletes lösen keine Model-Events aus).
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::deleting(function (UserAbo $abo): void {
|
||||
if ($abo->active) {
|
||||
$abo->active = false;
|
||||
$abo->saveQuietly();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'int',
|
||||
'member_id' => 'int',
|
||||
|
|
@ -105,6 +122,8 @@ class UserAbo extends Model
|
|||
'next_date' => 'datetime',
|
||||
'onetime_reminder_sent_for' => 'date',
|
||||
'cancel_date' => 'datetime',
|
||||
'last_pause_date' => 'date',
|
||||
'pause_count' => 'int',
|
||||
'user_deleted_at' => 'datetime',
|
||||
'carddata' => 'array',
|
||||
'card_update_pending_at' => 'datetime',
|
||||
|
|
@ -133,6 +152,8 @@ class UserAbo extends Model
|
|||
'next_date',
|
||||
'onetime_reminder_sent_for',
|
||||
'cancel_date',
|
||||
'last_pause_date',
|
||||
'pause_count',
|
||||
'user_deleted_at',
|
||||
'card_update_pending_at',
|
||||
];
|
||||
|
|
@ -355,6 +376,11 @@ class UserAbo extends Model
|
|||
return $this->hasMany(UserAboItem::class);
|
||||
}
|
||||
|
||||
public function status_histories()
|
||||
{
|
||||
return $this->hasMany(UserAboStatusHistory::class);
|
||||
}
|
||||
|
||||
public function one_time_items()
|
||||
{
|
||||
return $this->hasMany(UserAboOneTimeItem::class);
|
||||
|
|
|
|||
127
app/Models/UserAboStatusHistory.php
Normal file
127
app/Models/UserAboStatusHistory.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Protokoll der Status-Aktionen eines Abos (Pause, Kündigung, Reaktivierung):
|
||||
* wer (view + actor_user_id) hat wann welche Aktion ausgeführt und wie sich das
|
||||
* Ausführungsdatum dadurch verschoben hat.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_abo_id
|
||||
* @property string $action
|
||||
* @property string|null $view
|
||||
* @property int|null $actor_user_id
|
||||
* @property Carbon|null $previous_next_date
|
||||
* @property Carbon|null $new_next_date
|
||||
*/
|
||||
class UserAboStatusHistory extends Model
|
||||
{
|
||||
public const ACTION_PAUSED = 'paused';
|
||||
|
||||
public const ACTION_CANCELLED = 'cancelled';
|
||||
|
||||
public const ACTION_DEACTIVATED = 'deactivated';
|
||||
|
||||
public const ACTION_REACTIVATED = 'reactivated';
|
||||
|
||||
public static $actionLabels = [
|
||||
self::ACTION_PAUSED => 'abo_history.status_paused',
|
||||
self::ACTION_CANCELLED => 'abo_history.status_cancelled',
|
||||
self::ACTION_DEACTIVATED => 'abo_history.status_deactivated',
|
||||
self::ACTION_REACTIVATED => 'abo_history.status_reactivated',
|
||||
];
|
||||
|
||||
public static $actionColors = [
|
||||
self::ACTION_PAUSED => 'warning',
|
||||
self::ACTION_CANCELLED => 'danger',
|
||||
self::ACTION_DEACTIVATED => 'secondary',
|
||||
self::ACTION_REACTIVATED => 'success',
|
||||
];
|
||||
|
||||
/**
|
||||
* Formular-View → Kanal-Label/Farbe der Item-Historie (gleiche Optik im Protokoll).
|
||||
*/
|
||||
public static $viewChannelMap = [
|
||||
'me' => 'user_me',
|
||||
'ot' => 'user_ot',
|
||||
'portal' => 'portal',
|
||||
'admin' => 'admin',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'user_abo_id',
|
||||
'action',
|
||||
'view',
|
||||
'actor_user_id',
|
||||
'previous_next_date',
|
||||
'new_next_date',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_abo_id' => 'int',
|
||||
'actor_user_id' => 'int',
|
||||
'previous_next_date' => 'date',
|
||||
'new_next_date' => 'date',
|
||||
];
|
||||
|
||||
public function user_abo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserAbo::class);
|
||||
}
|
||||
|
||||
public function actor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'actor_user_id');
|
||||
}
|
||||
|
||||
public function getActionBadge(): string
|
||||
{
|
||||
$label = isset(self::$actionLabels[$this->action]) ? __(self::$actionLabels[$this->action]) : $this->action;
|
||||
$color = self::$actionColors[$this->action] ?? 'secondary';
|
||||
|
||||
return '<span class="badge badge-'.$color.'">'.$label.'</span>';
|
||||
}
|
||||
|
||||
public function getChannelBadge(): string
|
||||
{
|
||||
$channel = self::$viewChannelMap[$this->view] ?? 'system';
|
||||
$label = isset(UserAboItemHistory::$channelLabels[$channel]) ? __(UserAboItemHistory::$channelLabels[$channel]) : $channel;
|
||||
$color = UserAboItemHistory::$channelColors[$channel] ?? 'secondary';
|
||||
|
||||
return '<span class="badge badge-outline-'.$color.'">'.$label.'</span>';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
if ($this->action === self::ACTION_PAUSED && $this->previous_next_date && $this->new_next_date) {
|
||||
return __('abo_history.status_shift', [
|
||||
'from' => $this->previous_next_date->format(\Util::formatDateDB()),
|
||||
'to' => $this->new_next_date->format(\Util::formatDateDB()),
|
||||
]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getFormattedDate(): string
|
||||
{
|
||||
return $this->created_at ? $this->created_at->format(\Util::formatDateDB().' H:i') : '';
|
||||
}
|
||||
|
||||
public function getActorName(): string
|
||||
{
|
||||
if (! $this->actor) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$name = trim(($this->actor->account?->first_name ?? '').' '.($this->actor->account?->last_name ?? ''));
|
||||
|
||||
return $name !== '' ? $name : ($this->actor->email ?? '-');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Http\Controllers\Api\KasSLLController;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
|
@ -32,9 +32,10 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserShopOnSite[] $on_sites
|
||||
* @property-read Collection|UserShopOnSite[] $on_sites
|
||||
* @property-read int|null $on_sites_count
|
||||
* @property-read \App\User $user
|
||||
* @property-read User $user
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserShop findSimilarSlugs($attribute, $config, $slug)
|
||||
* @method static bool|null forceDelete()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserShop newModelQuery()
|
||||
|
|
@ -65,16 +66,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @method static \Illuminate\Database\Query\Builder|\App\Models\UserShop withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\UserShop withoutTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserShop withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
|
||||
*
|
||||
* @property array|null $trans
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserShop whereTrans($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserShop extends Model
|
||||
{
|
||||
protected $table = 'user_shops';
|
||||
private $is_online = NULL;
|
||||
private $ssl_certificate_sni = NULL;
|
||||
private $ssl_certificate_sni_is_active = NULL;
|
||||
|
||||
protected $casts = [
|
||||
'featured' => 'array',
|
||||
|
|
@ -86,15 +87,17 @@ class UserShop extends Model
|
|||
];
|
||||
|
||||
use SoftDeletes;
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
use Sluggable;
|
||||
|
||||
public function sluggable() : array
|
||||
|
||||
public function sluggable(): array
|
||||
{
|
||||
return [
|
||||
'slug' => [
|
||||
'source' => 'name'
|
||||
]
|
||||
'source' => 'name',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -103,18 +106,27 @@ class UserShop extends Model
|
|||
return $this->belongsTo('App\User', 'user_id');
|
||||
}
|
||||
|
||||
public function on_sites(){
|
||||
public function on_sites()
|
||||
{
|
||||
return $this->hasMany('App\Models\UserShopOnSite', 'user_shop_id', 'id');
|
||||
}
|
||||
|
||||
public function getActiveDateFormat(){
|
||||
if(!$this->attributes['active_date']){ return ""; }
|
||||
public function getActiveDateFormat()
|
||||
{
|
||||
if (! $this->attributes['active_date']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['active_date'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
public function getActiveDateFormatSmall(){
|
||||
if(!$this->attributes['active_date']){ return ""; }
|
||||
return Carbon::parse($this->attributes['active_date'])->format("d.m.Y");
|
||||
public function getActiveDateFormatSmall()
|
||||
{
|
||||
if (! $this->attributes['active_date']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['active_date'])->format('d.m.Y');
|
||||
}
|
||||
|
||||
public function getLang($key, $default = true)
|
||||
|
|
@ -123,6 +135,7 @@ class UserShop extends Model
|
|||
if ($lang == 'de') {
|
||||
return $this->{$key};
|
||||
}
|
||||
|
||||
return $this->getTrans($key, $lang, $default);
|
||||
}
|
||||
|
||||
|
|
@ -131,126 +144,70 @@ class UserShop extends Model
|
|||
if ($lang == 'de') {
|
||||
return $this->{$key};
|
||||
}
|
||||
if(!empty($this->trans[$lang][$key])){
|
||||
if (! empty($this->trans[$lang][$key])) {
|
||||
return $this->trans[$lang][$key];
|
||||
}
|
||||
if($default){
|
||||
return !empty($this->{$key}) ? $this->{$key} : '';
|
||||
if ($default) {
|
||||
return ! empty($this->{$key}) ? $this->{$key} : '';
|
||||
}
|
||||
return "";
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getSubdomain($session=true)
|
||||
public function getSubdomain($session = true)
|
||||
{
|
||||
if($session && \Session::has('user_shop_domain')){
|
||||
if ($session && \Session::has('user_shop_domain')) {
|
||||
return \Session::get('user_shop_domain');
|
||||
}
|
||||
return config('app.protocol').$this->attributes['slug'].".".config('app.domain').config('app.tld_care');
|
||||
|
||||
return config('app.protocol').$this->attributes['slug'].'.'.config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
public function getSubdomainStatus()
|
||||
public function getSubdomainAvailable()
|
||||
{
|
||||
if($this->is_online !== NULL){
|
||||
return $this->is_online;
|
||||
}
|
||||
$rCurlHandle = curl_init($this->attributes['slug'].'.mivita.care');
|
||||
|
||||
$kas = new KasController();
|
||||
$domain = 'mivita.care';
|
||||
$pra = array(
|
||||
'subdomain_name' => $this->attributes['slug'].".".$domain,
|
||||
'domain_name' => $domain,
|
||||
);
|
||||
curl_setopt($rCurlHandle, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($rCurlHandle, CURLOPT_HEADER, true);
|
||||
curl_setopt($rCurlHandle, CURLOPT_NOBODY, true);
|
||||
curl_setopt($rCurlHandle, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
//check if exisist
|
||||
$subdomain = $kas->action('get_subdomains', $pra);
|
||||
if(is_soap_fault($subdomain)){
|
||||
$strResponse = curl_exec($rCurlHandle);
|
||||
|
||||
curl_close($rCurlHandle);
|
||||
|
||||
if (! $strResponse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if(!empty($subdomain[0]['is_active']) && $subdomain[0]['is_active'] == 'Y'){
|
||||
$this->is_online = true;
|
||||
}else{
|
||||
$this->is_online = false;
|
||||
}
|
||||
|
||||
if(!empty($subdomain[0]['ssl_certificate_sni']) && $subdomain[0]['ssl_certificate_sni'] == 'Y'){
|
||||
$this->ssl_certificate_sni = true;
|
||||
}else{
|
||||
$this->ssl_certificate_sni = false;
|
||||
$this->userShopAddSSL($subdomain[0]['subdomain_name']);
|
||||
}
|
||||
|
||||
if(!empty($subdomain[0]['ssl_certificate_sni_is_active']) && $subdomain[0]['ssl_certificate_sni_is_active'] == 'j'){
|
||||
$this->ssl_certificate_sni_is_active = true;
|
||||
}else{
|
||||
$this->ssl_certificate_sni_is_active = false;
|
||||
}
|
||||
|
||||
return $this->is_online;
|
||||
}
|
||||
|
||||
public function userShopAddSSL($full_subdomain_name)
|
||||
{
|
||||
$kas = new KasController();
|
||||
$ssl = KasSLLController::getApiSSLParameter();
|
||||
$pra = array(
|
||||
'hostname' => $full_subdomain_name
|
||||
);
|
||||
$pra = array_merge($pra, $ssl);
|
||||
$value = $kas->action('update_ssl', $pra);
|
||||
return $value;
|
||||
}
|
||||
public function getSubdomainSslSin ()
|
||||
{
|
||||
return $this->ssl_certificate_sni;
|
||||
}
|
||||
public function getSubdomainSslSinActive ()
|
||||
{
|
||||
return $this->ssl_certificate_sni_is_active;
|
||||
}
|
||||
public function getSubdomainAvailable ()
|
||||
{
|
||||
$rCurlHandle = curl_init ( $this->attributes['slug'].".mivita.care" );
|
||||
|
||||
curl_setopt ( $rCurlHandle, CURLOPT_CONNECTTIMEOUT, 10 );
|
||||
curl_setopt ( $rCurlHandle, CURLOPT_HEADER, TRUE );
|
||||
curl_setopt ( $rCurlHandle, CURLOPT_NOBODY, TRUE );
|
||||
curl_setopt ( $rCurlHandle, CURLOPT_RETURNTRANSFER, TRUE );
|
||||
|
||||
$strResponse = curl_exec ( $rCurlHandle );
|
||||
|
||||
curl_close ( $rCurlHandle );
|
||||
|
||||
if ( !$strResponse )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$url = "http://".$this->attributes['slug'].".mivita.care/domain/check";
|
||||
if(@file_get_contents($url) != 'checked'){
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function isImage(){
|
||||
if(empty($this->attributes['filename']) || @$this->attributes['filename'] == null || @$this->attributes['filename'] == ""){
|
||||
return false;
|
||||
}
|
||||
if(!\Storage::disk('public')->has('images/shop/'.$this->filename)){
|
||||
$url = 'http://'.$this->attributes['slug'].'.mivita.care/domain/check';
|
||||
if (@file_get_contents($url) != 'checked') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getImage(){
|
||||
if($this->isImage()){
|
||||
public function isImage()
|
||||
{
|
||||
if (empty($this->attributes['filename']) || @$this->attributes['filename'] == null || @$this->attributes['filename'] == '') {
|
||||
return false;
|
||||
}
|
||||
if (! \Storage::disk('public')->has('images/shop/'.$this->filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getImage()
|
||||
{
|
||||
if ($this->isImage()) {
|
||||
$link = 'images/shop/'.$this->filename;
|
||||
|
||||
return '/storage/'.$link.'?=lm='.\Storage::disk('public')->lastModified($link);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboStatusHistory;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ class AboRepository extends BaseRepository
|
|||
*/
|
||||
public static function getLockDaysChange(): int
|
||||
{
|
||||
$days = (int) \App\Models\Setting::getContentBySlug('abo-lock-days-change');
|
||||
$days = (int) Setting::getContentBySlug('abo-lock-days-change');
|
||||
|
||||
return $days > 0 ? $days : self::DEFAULT_LOCK_DAYS_CHANGE;
|
||||
}
|
||||
|
|
@ -31,7 +33,7 @@ class AboRepository extends BaseRepository
|
|||
*/
|
||||
public static function getLockDaysPauseCancel(): int
|
||||
{
|
||||
$days = (int) \App\Models\Setting::getContentBySlug('abo-lock-days-pause-cancel');
|
||||
$days = (int) Setting::getContentBySlug('abo-lock-days-pause-cancel');
|
||||
|
||||
return $days > 0 ? $days : self::DEFAULT_LOCK_DAYS_PAUSE_CANCEL;
|
||||
}
|
||||
|
|
@ -51,6 +53,13 @@ class AboRepository extends BaseRepository
|
|||
if (isset($data['action'])) {
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
if ($this->validate($data)) {
|
||||
// Pause ist eine eigenständige Aktion: sie verschiebt nur next_date
|
||||
// und darf nicht von der anschließenden Liefertag-Neuberechnung
|
||||
// überschrieben werden.
|
||||
if (isset($data['abo_pause']) && $data['abo_pause'] == 'true') {
|
||||
return $this->pauseOneMonth($data);
|
||||
}
|
||||
|
||||
$this->updateStatus($data);
|
||||
$this->model->abo_interval = (int) $data['abo_interval'];
|
||||
$nextDate = $this->isAdminUpdate($data)
|
||||
|
|
@ -60,7 +69,7 @@ class AboRepository extends BaseRepository
|
|||
$this->model->save();
|
||||
|
||||
$daysUntilNext = AboHelper::calendarDaysUntil(now(), $nextDate);
|
||||
\Session()->flash('alert-warning', __('abo.warning_next_date_info', ['days' => $daysUntilNext, 'date' => $nextDate->format('d.m.Y')]));
|
||||
\Session()->flash('alert-warning', __('abo.warning_next_date_info', ['days' => $daysUntilNext, 'date' => $nextDate->format(\Util::formatDateDB())]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -80,9 +89,18 @@ class AboRepository extends BaseRepository
|
|||
|
||||
// Handle cancellation
|
||||
if (isset($data['abo_cancel']) && $data['abo_cancel'] == 'true') {
|
||||
// Mindestlaufzeit serverseitig durchsetzen — die UI blendet den Button nur aus
|
||||
if (! $isAdminUpdate && ! AboHelper::canCancelAbo($this->model, $data['view'] ?? 'user')) {
|
||||
\Session()->flash('alert-error', __('abo.info_min_duration_orders_left', [
|
||||
'count' => max(0, (int) AboHelper::getAboMinDuration() - $this->model->getCountPaidOrders()),
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert/gekündigt werden
|
||||
if (! $isAdminUpdate && $this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
$daysUntil = (int) now()->diffInDays(Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::getLockDaysPauseCancel()) {
|
||||
\Session()->flash('alert-error', __('abo.error_cancel_locked', ['days' => $daysUntil, 'lock' => self::getLockDaysPauseCancel()]));
|
||||
|
||||
|
|
@ -96,13 +114,15 @@ class AboRepository extends BaseRepository
|
|||
$this->model->cancel_date = now();
|
||||
$this->model->save();
|
||||
|
||||
$this->logStatusHistory(UserAboStatusHistory::ACTION_CANCELLED, $data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$active = (isset($data['abo_is_active']) && $data['abo_is_active']) ? true : false;
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert werden
|
||||
if (! $isAdminUpdate && $this->model->active && ! $active && $this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
$daysUntil = (int) now()->diffInDays(Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::getLockDaysPauseCancel()) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_locked', ['days' => $daysUntil, 'lock' => self::getLockDaysPauseCancel()]));
|
||||
|
||||
|
|
@ -115,6 +135,8 @@ class AboRepository extends BaseRepository
|
|||
$this->model->status = 6; // inactive
|
||||
$this->model->active = false;
|
||||
$this->model->save();
|
||||
|
||||
$this->logStatusHistory(UserAboStatusHistory::ACTION_DEACTIVATED, $data);
|
||||
}
|
||||
}
|
||||
if (! $this->model->active && $active) {
|
||||
|
|
@ -122,11 +144,88 @@ class AboRepository extends BaseRepository
|
|||
$this->model->status = 2; // okay
|
||||
$this->model->active = true;
|
||||
$this->model->save();
|
||||
|
||||
$this->logStatusHistory(UserAboStatusHistory::ACTION_REACTIVATED, $data);
|
||||
}
|
||||
}
|
||||
$this->model->active = $active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pausiert das Abo um genau einen Ausführungs-Zyklus: next_date wandert auf den
|
||||
* gleichen Liefertag des Folgemonats, das Abo bleibt aktiv (Status 2).
|
||||
* Regeln (außer Admin): Sperrfrist vor der Ausführung, Mindestlaufzeit und
|
||||
* max. 1 Pause pro X Monate ({@see AboHelper::canPauseAbo()}).
|
||||
*/
|
||||
private function pauseOneMonth(array $data): bool
|
||||
{
|
||||
// Zeilen-Lock + frisches Modell: zwei parallele Pause-Requests dürfen next_date
|
||||
// nicht beide auf Basis des alten Standes verschieben (Doppel-Pause).
|
||||
return \DB::transaction(function () use ($data): bool {
|
||||
$this->model = UserAbo::whereKey($this->model->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
return $this->pauseOneMonthLocked($data);
|
||||
});
|
||||
}
|
||||
|
||||
private function pauseOneMonthLocked(array $data): bool
|
||||
{
|
||||
$isAdminUpdate = $this->isAdminUpdate($data);
|
||||
|
||||
if (! $this->model->active || (int) $this->model->status !== 2 || ! $this->model->getRawOriginal('next_date')) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_not_active'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $isAdminUpdate) {
|
||||
$daysUntil = (int) now()->diffInDays(Carbon::parse($this->model->getRawOriginal('next_date')), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::getLockDaysPauseCancel()) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_locked', ['days' => $daysUntil, 'lock' => self::getLockDaysPauseCancel()]));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! AboHelper::canCancelAbo($this->model, $data['view'] ?? 'user')) {
|
||||
\Session()->flash('alert-error', __('abo.info_min_duration_orders_left', ['count' => (int) AboHelper::getAboMinDuration() - $this->model->getCountPaidOrders()]));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($nextPossible = AboHelper::nextPossiblePauseDate($this->model)) !== null) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_interval', ['months' => AboHelper::getPauseIntervalMonths(), 'date' => $nextPossible->format(\Util::formatDateDB())]));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$previousNextDate = Carbon::parse($this->model->getRawOriginal('next_date'));
|
||||
$newNextDate = AboHelper::setNextDate($previousNextDate, (int) $this->model->abo_interval);
|
||||
|
||||
$this->model->next_date = $newNextDate;
|
||||
$this->model->last_pause_date = now();
|
||||
$this->model->pause_count = (int) $this->model->pause_count + 1;
|
||||
$this->model->save();
|
||||
|
||||
$this->logStatusHistory(UserAboStatusHistory::ACTION_PAUSED, $data, $previousNextDate, $newNextDate);
|
||||
|
||||
\Session()->flash('alert-success', __('abo.pause_success', ['date' => $newNextDate->format(\Util::formatDateDB())]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function logStatusHistory(string $action, array $data, ?Carbon $previousNextDate = null, ?Carbon $newNextDate = null): void
|
||||
{
|
||||
UserAboStatusHistory::create([
|
||||
'user_abo_id' => $this->model->id,
|
||||
'action' => $action,
|
||||
'view' => $data['view'] ?? null,
|
||||
'actor_user_id' => \Auth::id(),
|
||||
'previous_next_date' => $previousNextDate,
|
||||
'new_next_date' => $newNextDate,
|
||||
]);
|
||||
}
|
||||
|
||||
private function validate($data)
|
||||
{
|
||||
if ($data['view'] !== 'admin' && $data['view'] !== 'portal') {
|
||||
|
|
@ -151,7 +250,7 @@ class AboRepository extends BaseRepository
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (! isset($data['abo_interval']) || ! in_array((int) $data['abo_interval'], \App\Models\UserAbo::$aboDeliveryDays, true)) {
|
||||
if (! isset($data['abo_interval']) || ! in_array((int) $data['abo_interval'], UserAbo::$aboDeliveryDays, true)) {
|
||||
\Session()->flash('alert-error', __('abo.error_abo_interval'));
|
||||
|
||||
return false;
|
||||
|
|
@ -181,7 +280,7 @@ class AboRepository extends BaseRepository
|
|||
if ($intervalChanged) {
|
||||
// Sperre: X Tage vor nächster Ausführung kein Liefertag-Wechsel mehr (Pakete werden vorgepackt)
|
||||
if ($this->model->next_date) {
|
||||
$daysUntilExecution = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
$daysUntilExecution = (int) now()->diffInDays(Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntilExecution >= 0 && $daysUntilExecution < self::getLockDaysChange()) {
|
||||
\Session()->flash('alert-error', __('abo.error_change_locked', ['days' => $daysUntilExecution, 'lock' => self::getLockDaysChange()]));
|
||||
|
||||
|
|
@ -208,12 +307,12 @@ class AboRepository extends BaseRepository
|
|||
* Falls next_date bereits in einem zukünftigen Monat liegt, wird der Monatsanfang
|
||||
* dieses Monats als Referenz verwendet, sodass der neue Tag im selben Monat landet.
|
||||
*/
|
||||
private function calculateNewNextDate(int $aboInterval): \Carbon\Carbon
|
||||
private function calculateNewNextDate(int $aboInterval): Carbon
|
||||
{
|
||||
$referenceDate = now();
|
||||
|
||||
if ($this->model->next_date) {
|
||||
$currentNextDate = \Carbon\Carbon::parse($this->model->next_date);
|
||||
$currentNextDate = Carbon::parse($this->model->next_date);
|
||||
|
||||
if ($currentNextDate->format('Y-m') > now()->format('Y-m')) {
|
||||
$referenceDate = $currentNextDate->startOfMonth();
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ class CreditRepository extends BaseRepository
|
|||
|
||||
public function create($request = [])
|
||||
{
|
||||
// need invoice $data
|
||||
$number = Credit::getCreditNumber();
|
||||
// Nummer atomar reservieren (gemeinsamer Nummernkreis mit Rechnungen)
|
||||
$number = Credit::makeNextCreditNumber();
|
||||
$credit_date = isset($request['credit_date']) ? $request['credit_date'] : \Carbon::now()->format('d.m.Y');
|
||||
$credit_send_mail = isset($request['credit_send_mail']) ? true : false;
|
||||
$credit_number = Credit::createCreditNumber($number, $credit_date);
|
||||
|
|
@ -131,8 +131,6 @@ class CreditRepository extends BaseRepository
|
|||
|
||||
private function finishUserCredit($user_credit_id, $user_credit_items)
|
||||
{
|
||||
// next credits
|
||||
Credit::makeNextCreditNumber();
|
||||
// mark as payed
|
||||
// $UserCreditItems = UserCreditItem::where('user_id', $this->model->id)->wherePaid(false)->get();
|
||||
foreach ($user_credit_items as $user_credit_item) {
|
||||
|
|
|
|||
|
|
@ -2,20 +2,16 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Util;
|
||||
use Request;
|
||||
use App\User;
|
||||
use Validator;
|
||||
use App\Services\UserUtil;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Models\UserAccount;
|
||||
use App\Services\UserUtil;
|
||||
use App\Services\VatIdService;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use Validator;
|
||||
|
||||
class UserRepository extends BaseRepository
|
||||
{
|
||||
|
||||
|
||||
public function __construct(User $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
|
@ -24,7 +20,7 @@ class UserRepository extends BaseRepository
|
|||
public function update($data)
|
||||
{
|
||||
|
||||
if ($data['user_id'] === "new" || $data['user_id'] == 0) {
|
||||
if ($data['user_id'] === 'new' || $data['user_id'] == 0) {
|
||||
$this->model = User::create([
|
||||
'email' => $data['email'],
|
||||
'password' => env('APP_KEY'),
|
||||
|
|
@ -35,16 +31,16 @@ class UserRepository extends BaseRepository
|
|||
$this->model = $this->getById($data['user_id']);
|
||||
}
|
||||
|
||||
if (!$this->model->account_id) {
|
||||
$account = new UserAccount();
|
||||
if (! $this->model->account_id) {
|
||||
$account = new UserAccount;
|
||||
} else {
|
||||
$account = $this->model->account;
|
||||
}
|
||||
|
||||
$data['same_as_billing'] = !isset($data['same_as_billing']) ? 0 : 1;
|
||||
$data['same_as_billing'] = ! isset($data['same_as_billing']) ? 0 : 1;
|
||||
$account->fill($data)->save();
|
||||
|
||||
if (!$this->model->account_id) {
|
||||
if (! $this->model->account_id) {
|
||||
$this->model->account_id = $account->id;
|
||||
$this->model->save();
|
||||
}
|
||||
|
|
@ -73,7 +69,6 @@ class UserRepository extends BaseRepository
|
|||
$this->model->payment_methods = PaymentMethod::getDefaultAsArray();
|
||||
$this->model->save();
|
||||
|
||||
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
|
|
@ -91,31 +86,37 @@ class UserRepository extends BaseRepository
|
|||
{
|
||||
|
||||
if (isset($data['action']) && $data['action'] == 'reverse_charge_validate') {
|
||||
$rules = array(
|
||||
$rules = [
|
||||
'tax_identification_number' => 'required',
|
||||
);
|
||||
];
|
||||
$validator = Validator::make($data, $rules);
|
||||
if ($validator->fails()) {
|
||||
$data = [
|
||||
'user' => $user,
|
||||
];
|
||||
return redirect($route . '#user-vat-validation')->withErrors($validator)->withInput($data);
|
||||
|
||||
return redirect($route.'#user-vat-validation')->withErrors($validator)->withInput($data);
|
||||
}
|
||||
$ret = $this->reverse_charge_activate($data, $user);
|
||||
if ($ret === 'error') {
|
||||
if ($ret === 'error' || $ret === 'error_german') {
|
||||
$validator = Validator::make($data, []);
|
||||
$validator->errors()->add('tax_identification_number_validated', __('msg.VATID_could_not_be_validated'));
|
||||
$message = $ret === 'error_german'
|
||||
? __('msg.VATID_german_not_allowed')
|
||||
: __('msg.VATID_could_not_be_validated');
|
||||
$validator->errors()->add('tax_identification_number_validated', $message);
|
||||
$data['reverse_charge'] = 0;
|
||||
$data = [
|
||||
'user' => $user,
|
||||
];
|
||||
return redirect($route . '#user-vat-validation')->withErrors($validator)->withInput($data);
|
||||
|
||||
return redirect($route.'#user-vat-validation')->withErrors($validator)->withInput($data);
|
||||
}
|
||||
if ($ret === 'valid') {
|
||||
\Session()->flash('alert-success', __('msg.VATID_successfully_entered'));
|
||||
return redirect($route . '#user-vat-validation')->withInput($data);
|
||||
|
||||
return redirect($route . '#user-vat-validation')->withInput($data);
|
||||
return redirect($route.'#user-vat-validation')->withInput($data);
|
||||
|
||||
return redirect($route.'#user-vat-validation')->withInput($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,12 +126,13 @@ class UserRepository extends BaseRepository
|
|||
if (isset($data['action']) && $data['action'] == 'reverse_charge_delete') {
|
||||
$user->account->tax_identification_number = '';
|
||||
$user->account->reverse_charge = 0;
|
||||
$user->account->reverse_charge_code = null;
|
||||
$user->account->reverse_charge_code = null;
|
||||
$user->account->reverse_charge_valid = null;
|
||||
$user->account->save();
|
||||
$data['tax_identification_number'] = '';
|
||||
\Session()->flash('alert-success', __('msg.reverse_charge_procedure_and_VATID_deleted'));
|
||||
return redirect($route . '#user-vat-validation')->withInput($data);
|
||||
|
||||
return redirect($route.'#user-vat-validation')->withInput($data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,42 +167,30 @@ class UserRepository extends BaseRepository
|
|||
'SI' => 'SI-Slowenien',
|
||||
'SK' => 'SK-Slowakei',
|
||||
'XI' => 'XI-Nordirland', */
|
||||
$countryCode = 'DE';
|
||||
$accountCountryCode = 'DE';
|
||||
|
||||
if ($user->account->country_id) {
|
||||
$countryCode = $user->account->country->code;
|
||||
$accountCountryCode = $user->account->country->code;
|
||||
}
|
||||
|
||||
$vatid = str_replace(array(' ', '.', '-', ',', ', '), '', trim($data['tax_identification_number']));
|
||||
$cc = substr($vatid, 0, 2);
|
||||
$vatNo = substr($vatid, 2);
|
||||
$vatService = app(VatIdService::class);
|
||||
$vatid = $vatService->normalize($data['tax_identification_number']);
|
||||
$countryCode = $vatService->extractCountryCode($vatid, $accountCountryCode);
|
||||
|
||||
$options = [
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
'trace' => 1,
|
||||
'stream_context' => stream_context_create(
|
||||
[
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
]
|
||||
]
|
||||
)
|
||||
];
|
||||
if ($vatService->isGermanVatId($vatid) || $countryCode === 'DE') {
|
||||
return 'error_german';
|
||||
}
|
||||
|
||||
$client = new \SoapClient("https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl", $options);
|
||||
$result = $client->checkVat(['countryCode' => $countryCode, 'vatNumber' => $vatNo]);
|
||||
|
||||
if ($result->valid == true) {
|
||||
if ($vatService->checkVies($vatid, $accountCountryCode) === VatIdService::CHECK_VALID) {
|
||||
$user->account->tax_identification_number = $data['tax_identification_number'];
|
||||
$user->account->reverse_charge = 1;
|
||||
$user->account->reverse_charge_code = $countryCode;
|
||||
$user->account->reverse_charge_valid = now();
|
||||
$user->account->save();
|
||||
|
||||
return 'valid';
|
||||
} else {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
250
app/Services/Abo/PaylineForecastService.php
Normal file
250
app/Services/Abo/PaylineForecastService.php
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Abo;
|
||||
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\Services\Backoffice\SalesVolumePoints;
|
||||
use App\Services\BusinessPlan\LiveCommissionService;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* Payline-bezogene Abo-Kennzahlen (Konzept B8 + B10):
|
||||
*
|
||||
* - Payline = die ersten N Ebenen der Downline; N ("paylines") kommt aus dem
|
||||
* Provisionslevel des Users (Live-TreeCalcBot bzw. UserBusiness-Snapshot).
|
||||
* - B8 aboTurnover(): Abo-Gesamtumsatz der Payline als Soll (Abo-Punkte-Bestand
|
||||
* pro Zyklus) und Ist (im Monat abgerechnete Abo-Punkte, F10).
|
||||
* - B10 forecastDays(): voraussichtlicher Payline-Gesamtumsatz zu den festen
|
||||
* Ausführungsterminen 5./10./20./25. — Basis (Kundenumsatz + Payline-Punkte
|
||||
* Stand heute) plus die bis zum jeweiligen Termin noch anstehenden
|
||||
* Abo-Punkte, kumuliert (F11).
|
||||
*/
|
||||
class PaylineForecastService
|
||||
{
|
||||
/**
|
||||
* F12 (dokumentierte Annahme, Kunde entscheidet nach Umsetzung): Die eigenen
|
||||
* Abos und eigenen Kundenabos des Beraters zählen in der Prognose MIT, weil
|
||||
* die Basis der Formel den aktuellen Kundenumsatz enthält und kommende
|
||||
* eigene Ausführungen genau diesen Anteil erhöhen. Über diesen Schalter
|
||||
* ohne Umbau umkehrbar.
|
||||
*/
|
||||
public const INCLUDE_OWN_ABOS = true;
|
||||
|
||||
private const CACHE_HOURS = 6;
|
||||
|
||||
public function __construct(
|
||||
private BackofficeDashboardService $dashboardService,
|
||||
private LiveCommissionService $commissionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Payline-Kontext (Anzahl bezahlter Ebenen + Mitglieder-IDs), 6 h gecacht —
|
||||
* die Payline-Tiefe kommt aus dem gemeinsamen Live-Lauf, die Abo-Summen
|
||||
* bleiben live. Ein fehlgeschlagener Live-Lauf wird nicht gecacht, damit
|
||||
* ein transienter Fehler die Karte nicht 6 h lang leert.
|
||||
*
|
||||
* @return array{paylines: int, member_ids: int[]}
|
||||
*/
|
||||
public function paylineContext(User $user, int $month, int $year): array
|
||||
{
|
||||
$cacheKey = "payline_context_{$user->id}_{$month}_{$year}";
|
||||
$cached = Cache::get($cacheKey);
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
[$context, $failed] = $this->buildPaylineContext($user, $month, $year);
|
||||
|
||||
if (! $failed) {
|
||||
Cache::put($cacheKey, $context, now()->addHours(self::CACHE_HOURS));
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* B8: Abo-Gesamtumsatz in der Payline — Soll und Ist, plus die Anzahl der
|
||||
* dahinterstehenden Abos (Berater- und Kundenabos, Kundenwunsch 2026-07-24).
|
||||
*
|
||||
* @return array{paylines: int, members: int, target: float, actual: float, team_abo_count: int, team_customer_abo_count: int}
|
||||
*/
|
||||
public function aboTurnover(User $user, int $month, int $year): array
|
||||
{
|
||||
$context = $this->paylineContext($user, $month, $year);
|
||||
$memberIds = $context['member_ids'];
|
||||
|
||||
return [
|
||||
'paylines' => $context['paylines'],
|
||||
'members' => count($memberIds),
|
||||
'target' => $this->aboTargetPoints($memberIds),
|
||||
'actual' => $this->aboActualPoints($memberIds, $month, $year),
|
||||
'team_abo_count' => $this->aboCount($memberIds, 'me', 'user_id'),
|
||||
'team_customer_abo_count' => $this->aboCount($memberIds, 'ot', 'member_id'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzahl aktiver Abos der Payline-Mitglieder (gleiche Filter wie Soll).
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
*/
|
||||
private function aboCount(array $memberIds, string $isFor, string $userColumn): int
|
||||
{
|
||||
if ($memberIds === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return UserAbo::query()
|
||||
->where('active', true)
|
||||
->whereNotIn('status', [4, 5, 6])
|
||||
->where('is_for', $isFor)
|
||||
->whereIn($userColumn, $memberIds)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* B10: Prognose je Ausführungstermin des LAUFENDEN Monats. Basis ist der
|
||||
* heutige Stand (Kundenumsatz + Payline-Punkte, vom Aufrufer übergeben);
|
||||
* dazu kommen kumuliert die Abo-Punkte aller noch offenen Ausführungen bis
|
||||
* zum jeweiligen Termin (next_date > heute, im laufenden Monat). Bereits
|
||||
* ausgeführte Abos stecken im Ist-Umsatz und werden nicht doppelt gezählt.
|
||||
*
|
||||
* @return array{base: float, days: array<int, array{day: int, date: string, upcoming: float, total: float, is_past: bool}>}
|
||||
*/
|
||||
public function forecastDays(User $user, float $basePoints): array
|
||||
{
|
||||
$today = Carbon::today();
|
||||
$context = $this->paylineContext($user, (int) $today->month, (int) $today->year);
|
||||
|
||||
$upcomingAbos = $this->upcomingAboQuery($user, $context['member_ids'], $today)
|
||||
->with('user_abo_items.product')
|
||||
->get();
|
||||
|
||||
$pointsByDay = [];
|
||||
foreach ($upcomingAbos as $abo) {
|
||||
$day = (int) Carbon::parse($abo->getRawOriginal('next_date'))->day;
|
||||
$pointsByDay[$day] = ($pointsByDay[$day] ?? 0.0) + (float) $abo->getTotalPoints();
|
||||
}
|
||||
|
||||
$days = [];
|
||||
foreach (UserAbo::$aboDeliveryDays as $deliveryDay) {
|
||||
$upcoming = 0.0;
|
||||
foreach ($pointsByDay as $day => $points) {
|
||||
if ($day <= $deliveryDay) {
|
||||
$upcoming += $points;
|
||||
}
|
||||
}
|
||||
|
||||
$days[] = [
|
||||
'day' => $deliveryDay,
|
||||
'date' => $today->copy()->startOfMonth()->addDays($deliveryDay - 1)->format(\Util::formatDateDB()),
|
||||
'upcoming' => $upcoming,
|
||||
'total' => $basePoints + $upcoming,
|
||||
'is_past' => $deliveryDay < (int) $today->day,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'base' => $basePoints,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: array{paylines: int, member_ids: int[]}, 1: bool} [Kontext, Lauf fehlgeschlagen]
|
||||
*/
|
||||
private function buildPaylineContext(User $user, int $month, int $year): array
|
||||
{
|
||||
$snapshot = $this->commissionService->snapshot($user, $month, $year);
|
||||
$paylines = $snapshot['paylines'];
|
||||
|
||||
if ($paylines <= 0) {
|
||||
return [['paylines' => 0, 'member_ids' => []], (bool) ($snapshot['failed'] ?? false)];
|
||||
}
|
||||
|
||||
$memberIds = collect($this->dashboardService->lineBuckets($user->id))
|
||||
->filter(fn ($users, int $line) => $line <= $paylines)
|
||||
->flatMap(fn ($users) => $users->pluck('id'))
|
||||
->map(fn ($id) => (int) $id)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [['paylines' => $paylines, 'member_ids' => $memberIds], false];
|
||||
}
|
||||
|
||||
/**
|
||||
* Soll: Abo-Punkte-Bestand (Produktpunkte × Menge, Hauptartikel comp = 0)
|
||||
* aller aktiven Abos der Payline-Mitglieder — Berater-Abos und Kundenabos.
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
*/
|
||||
private function aboTargetPoints(array $memberIds): float
|
||||
{
|
||||
if ($memberIds === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) UserAbo::query()
|
||||
->join('user_abo_items', 'user_abo_items.user_abo_id', '=', 'user_abos.id')
|
||||
->join('products', 'products.id', '=', 'user_abo_items.product_id')
|
||||
->whereNull('products.deleted_at')
|
||||
->where('user_abos.active', true)
|
||||
->whereNotIn('user_abos.status', [4, 5, 6])
|
||||
->where('user_abo_items.comp', 0)
|
||||
->where(function (Builder $query) use ($memberIds) {
|
||||
$query->where(fn (Builder $inner) => $inner->where('user_abos.is_for', 'me')->whereIn('user_abos.user_id', $memberIds))
|
||||
->orWhere(fn (Builder $inner) => $inner->where('user_abos.is_for', 'ot')->whereIn('user_abos.member_id', $memberIds));
|
||||
})
|
||||
->selectRaw('COALESCE(SUM(products.points * user_abo_items.qty), 0) as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist: im Monat abgerechnete Abo-Punkte der Payline-Mitglieder.
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
*/
|
||||
private function aboActualPoints(array $memberIds, int $month, int $year): float
|
||||
{
|
||||
if ($memberIds === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) UserSalesVolume::query()
|
||||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->whereIn('user_id', $memberIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->selectRaw(SalesVolumePoints::aboPoints().' as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Noch offene Abo-Ausführungen des laufenden Monats: aktive Abos der
|
||||
* Payline-Mitglieder plus — je nach {@see INCLUDE_OWN_ABOS} — die eigenen
|
||||
* Abos/Kundenabos des Betrachters.
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
*/
|
||||
private function upcomingAboQuery(User $user, array $memberIds, Carbon $today): Builder
|
||||
{
|
||||
$aboUserIds = self::INCLUDE_OWN_ABOS
|
||||
? array_values(array_unique(array_merge([(int) $user->id], $memberIds)))
|
||||
: $memberIds;
|
||||
|
||||
return UserAbo::query()
|
||||
->where('active', true)
|
||||
->where('status', 2)
|
||||
->where('next_date', '>', $today->copy()->endOfDay())
|
||||
->where('next_date', '<=', $today->copy()->endOfMonth())
|
||||
->where(function (Builder $query) use ($aboUserIds) {
|
||||
$query->where(fn (Builder $inner) => $inner->where('is_for', 'me')->whereIn('user_id', $aboUserIds))
|
||||
->orWhere(fn (Builder $inner) => $inner->where('is_for', 'ot')->whereIn('member_id', $aboUserIds));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,14 @@
|
|||
namespace App\Services\Abo;
|
||||
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\Services\Backoffice\SalesVolumePoints;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +64,10 @@ class TeamAboOverviewService
|
|||
{
|
||||
$teamUserIds = $this->dashboardService->activeTeamUserIds($user->id);
|
||||
|
||||
// 1000-Punkte-Shop: der Berater selbst zählt mit (Kundenwunsch), alle
|
||||
// übrigen Team-Kennzahlen bleiben ohne den Betrachter.
|
||||
$shop1000UserIds = array_merge([(int) $user->id], $teamUserIds);
|
||||
|
||||
if ($teamUserIds === []) {
|
||||
return [
|
||||
'team_total_abos' => 0,
|
||||
|
|
@ -66,7 +75,7 @@ class TeamAboOverviewService
|
|||
'team_partner_abos_new' => 0,
|
||||
'team_customer_abos' => 0,
|
||||
'team_customer_abos_new' => 0,
|
||||
'shop_1000' => 0,
|
||||
'shop_1000' => $this->dashboardService->shop1000Count($shop1000UserIds, $month, $year),
|
||||
'new_partners' => 0,
|
||||
];
|
||||
}
|
||||
|
|
@ -80,7 +89,7 @@ class TeamAboOverviewService
|
|||
'team_partner_abos_new' => $this->newAboCount($teamUserIds, 'user_id', 'me', $month, $year),
|
||||
'team_customer_abos' => $customerAbos,
|
||||
'team_customer_abos_new' => $this->newAboCount($teamUserIds, 'member_id', 'ot', $month, $year),
|
||||
'shop_1000' => $this->dashboardService->shop1000Count($teamUserIds, $month, $year),
|
||||
'shop_1000' => $this->dashboardService->shop1000Count($shop1000UserIds, $month, $year),
|
||||
'new_partners' => $this->dashboardService->newPartnersCount($teamUserIds, $month, $year),
|
||||
];
|
||||
}
|
||||
|
|
@ -117,7 +126,7 @@ class TeamAboOverviewService
|
|||
public function monthlyGrowth(User $user, int $year): array
|
||||
{
|
||||
$teamUserIds = $this->dashboardService->activeTeamUserIds($user->id);
|
||||
$monthNames = \App\Services\HTMLHelper::getTransMonths();
|
||||
$monthNames = HTMLHelper::getTransMonths();
|
||||
|
||||
$ownByMonth = $teamUserIds === [] ? [] : $this->newAbosByMonth($teamUserIds, 'user_id', 'me', $year);
|
||||
$customerByMonth = $teamUserIds === [] ? [] : $this->newAbosByMonth($teamUserIds, 'member_id', 'ot', $year);
|
||||
|
|
@ -191,7 +200,7 @@ class TeamAboOverviewService
|
|||
*/
|
||||
public function memberAboSummary(User $owner, int $memberId): ?array
|
||||
{
|
||||
if (! \App\Services\AboHelper::isUserInTeam($owner->id, $memberId)) {
|
||||
if (! AboHelper::isUserInTeam($owner->id, $memberId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +254,7 @@ class TeamAboOverviewService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
* @param Collection<int, User> $users
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildLineRow(int $line, Collection $users, int $month, int $year): array
|
||||
|
|
@ -303,6 +312,8 @@ class TeamAboOverviewService
|
|||
];
|
||||
}
|
||||
|
||||
$billedPoints = $this->aboBilledPoints($userIds, $month, $year);
|
||||
|
||||
return [
|
||||
'line' => $line,
|
||||
'label' => __('abo.team_line', ['line' => $line]),
|
||||
|
|
@ -315,16 +326,80 @@ class TeamAboOverviewService
|
|||
'customer_abos' => $customerAbosCount,
|
||||
'customer_abos_new' => $this->newAboCount($userIds, 'member_id', 'ot', $month, $year),
|
||||
'customer_abo_points' => $customerAboPoints,
|
||||
'abo_points_billed' => $billedPoints,
|
||||
'abo_points_expected' => $billedPoints + $this->upcomingAboPointsUntilLastDeliveryDay($userIds, $month, $year),
|
||||
'total_abos' => $ownAbosCount + $customerAbosCount,
|
||||
'members' => $members,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Im Monat bereits abgerechnete Abo-Punkte (Eigen- und Kundenabos) der
|
||||
* übergebenen Berater — gleiche Zählweise wie der Payline-Ist-Wert
|
||||
* ({@see PaylineForecastService::aboActualPoints()}).
|
||||
*
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
private function aboBilledPoints(array $userIds, int $month, int $year): float
|
||||
{
|
||||
if ($userIds === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) UserSalesVolume::query()
|
||||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->selectRaw(SalesVolumePoints::aboPoints().' as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Abo-Punkte der noch offenen Ausführungen bis einschließlich zum letzten
|
||||
* Liefertag (25.) des Monats: aktive Abos der Berater mit next_date nach
|
||||
* heute und im angefragten Monat. Für vergangene/zukünftige Monate 0 —
|
||||
* die Prognose ergibt nur für den laufenden Monat Sinn.
|
||||
*
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
private function upcomingAboPointsUntilLastDeliveryDay(array $userIds, int $month, int $year): float
|
||||
{
|
||||
$today = Carbon::today();
|
||||
|
||||
if ($userIds === [] || (int) $today->month !== $month || (int) $today->year !== $year) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$lastDeliveryDay = (int) max(UserAbo::$aboDeliveryDays);
|
||||
$windowEnd = Carbon::create($year, $month, 1)->addDays($lastDeliveryDay - 1)->endOfDay();
|
||||
|
||||
if ($windowEnd->lte($today->copy()->endOfDay())) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) UserAbo::query()
|
||||
->join('user_abo_items', 'user_abo_items.user_abo_id', '=', 'user_abos.id')
|
||||
->join('products', 'products.id', '=', 'user_abo_items.product_id')
|
||||
->whereNull('products.deleted_at')
|
||||
->where('user_abos.active', true)
|
||||
->where('user_abos.status', 2)
|
||||
->where('user_abo_items.comp', 0)
|
||||
->where('user_abos.next_date', '>', $today->copy()->endOfDay())
|
||||
->where('user_abos.next_date', '<=', $windowEnd)
|
||||
->where(function (Builder $query) use ($userIds) {
|
||||
$query->where(fn (Builder $inner) => $inner->where('user_abos.is_for', 'me')->whereIn('user_abos.user_id', $userIds))
|
||||
->orWhere(fn (Builder $inner) => $inner->where('user_abos.is_for', 'ot')->whereIn('user_abos.member_id', $userIds));
|
||||
})
|
||||
->selectRaw('COALESCE(SUM(products.points * user_abo_items.qty), 0) as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eigene (me) aktive Abos je Berater, indiziert nach user_id (ein Abo pro Berater).
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @return \Illuminate\Support\Collection<int, \App\Models\UserAbo>
|
||||
* @return Collection<int, UserAbo>
|
||||
*/
|
||||
private function ownAbosByUser(array $userIds): Collection
|
||||
{
|
||||
|
|
@ -341,7 +416,7 @@ class TeamAboOverviewService
|
|||
* Kundenabos (ot) je Berater, gruppiert nach member_id.
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, \App\Models\UserAbo>>
|
||||
* @return Collection<int, Collection<int, UserAbo>>
|
||||
*/
|
||||
private function customerAbosByMember(array $userIds): Collection
|
||||
{
|
||||
|
|
@ -399,6 +474,8 @@ class TeamAboOverviewService
|
|||
'customer_abos' => 0,
|
||||
'customer_abos_new' => 0,
|
||||
'customer_abo_points' => 0.0,
|
||||
'abo_points_billed' => 0.0,
|
||||
'abo_points_expected' => 0.0,
|
||||
'total_abos' => 0,
|
||||
'members' => [],
|
||||
];
|
||||
|
|
@ -411,7 +488,7 @@ class TeamAboOverviewService
|
|||
*/
|
||||
private function addToTotals(array $totals, array $row): array
|
||||
{
|
||||
foreach (['consultants', 'members_total', 'members_with_own_abo', 'own_abos', 'own_abos_new', 'own_abo_points', 'customer_abos', 'customer_abos_new', 'customer_abo_points', 'total_abos'] as $key) {
|
||||
foreach (['consultants', 'members_total', 'members_with_own_abo', 'own_abos', 'own_abos_new', 'own_abo_points', 'customer_abos', 'customer_abos_new', 'customer_abo_points', 'abo_points_billed', 'abo_points_expected', 'total_abos'] as $key) {
|
||||
$totals[$key] += $row[$key];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,26 @@ class AboHelper
|
|||
*/
|
||||
public const DEFAULT_ONETIME_WINDOW_DAYS = 4;
|
||||
|
||||
/**
|
||||
* Standard-Mindestlaufzeit (bezahlte Lieferungen) bis zur Kündigung, falls
|
||||
* das Setting `abo-min-duration` nicht gepflegt ist.
|
||||
*/
|
||||
public const DEFAULT_ABO_MIN_DURATION = 6;
|
||||
|
||||
/**
|
||||
* Abo-Status, die ein Abo endgültig beenden (4 = gekündigt, 5 = beendet).
|
||||
* Solche Abos blockieren die Anlage eines neuen Abos nicht mehr
|
||||
* (Ein-Abo-Regel gilt nur für laufende Abos).
|
||||
*/
|
||||
public const ENDED_STATUSES = [4, 5];
|
||||
|
||||
/**
|
||||
* Standard-Abstand (Monate) zwischen zwei Pausen, falls das Setting
|
||||
* `abo-pause-interval-months` nicht gepflegt ist (max. 1 Pause pro X Monate,
|
||||
* rollierend ab der letzten Pause).
|
||||
*/
|
||||
public const DEFAULT_PAUSE_INTERVAL_MONTHS = 6;
|
||||
|
||||
public static $txaction_filter_text = [
|
||||
'paid' => 'paymend_paid',
|
||||
'appointed' => 'paymend_open',
|
||||
|
|
@ -48,7 +68,7 @@ class AboHelper
|
|||
{
|
||||
$user = $user ? $user : \Auth::user();
|
||||
|
||||
return UserAbo::where('user_id', $user->id)->where('is_for', 'me')->where('status', '>=', 1)->first() === null ? false : true;
|
||||
return UserAbo::where('user_id', $user->id)->where('is_for', 'me')->where('status', '>=', 1)->whereNotIn('status', self::ENDED_STATUSES)->first() === null ? false : true;
|
||||
}
|
||||
|
||||
public static function memberHasAbo(ShoppingUser $shopping_user)
|
||||
|
|
@ -57,12 +77,12 @@ class AboHelper
|
|||
return false;
|
||||
}
|
||||
|
||||
return UserAbo::where('email', $shopping_user->billing_email)->where('is_for', 'ot')->where('status', '>=', 1)->first() === null ? false : true;
|
||||
return UserAbo::where('email', $shopping_user->billing_email)->where('is_for', 'ot')->where('status', '>=', 1)->whereNotIn('status', self::ENDED_STATUSES)->first() === null ? false : true;
|
||||
}
|
||||
|
||||
public static function hasAboByEmail($email)
|
||||
{
|
||||
return UserAbo::where('email', $email)->where('status', '>=', 1)->first() === null ? false : true;
|
||||
return UserAbo::where('email', $email)->where('status', '>=', 1)->whereNotIn('status', self::ENDED_STATUSES)->first() === null ? false : true;
|
||||
}
|
||||
|
||||
public static function setAboStatus(ShoppingOrder $shopping_order, $status, $paid = false)
|
||||
|
|
@ -97,7 +117,9 @@ class AboHelper
|
|||
|
||||
public static function getAboMinDuration()
|
||||
{
|
||||
return Setting::getContentBySlug('abo-min-duration');
|
||||
$minDuration = Setting::getContentBySlug('abo-min-duration');
|
||||
|
||||
return $minDuration === false || $minDuration === null || $minDuration === '' ? self::DEFAULT_ABO_MIN_DURATION : $minDuration;
|
||||
}
|
||||
|
||||
public static function canCancelAbo(UserAbo $user_abo, $view = 'user')
|
||||
|
|
@ -120,6 +142,59 @@ class AboHelper
|
|||
return ! self::canCancelAbo($user_abo, $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfigurierter Mindestabstand (Monate) zwischen zwei Pausen.
|
||||
*/
|
||||
public static function getPauseIntervalMonths(): int
|
||||
{
|
||||
$months = (int) Setting::getContentBySlug('abo-pause-interval-months');
|
||||
|
||||
return $months > 0 ? $months : self::DEFAULT_PAUSE_INTERVAL_MONTHS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf das Abo aktuell um einen Monat pausiert werden?
|
||||
* Voraussetzungen (außer Admin): Abo läuft (aktiv, Status 2), Mindestlaufzeit
|
||||
* erreicht (gleiche Basis wie die Kündigung, Setting `abo-min-duration`) und
|
||||
* die letzte Pause liegt mindestens {@see getPauseIntervalMonths()} Monate
|
||||
* zurück (rollierend). Die Sperrfrist vor der Ausführung
|
||||
* (`abo-lock-days-pause-cancel`) wird separat in der Pause-Aktion geprüft.
|
||||
*/
|
||||
public static function canPauseAbo(UserAbo $user_abo, $view = 'user'): bool
|
||||
{
|
||||
if (! $user_abo->active || (int) $user_abo->status !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($view === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! self::canCancelAbo($user_abo, $view)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::nextPossiblePauseDate($user_abo) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frühestes Datum, ab dem wieder pausiert werden darf — oder null, wenn die
|
||||
* Pause bereits jetzt möglich ist (keine oder ausreichend lange zurückliegende
|
||||
* letzte Pause). Dient auch dem UI-Hinweis "wieder möglich ab".
|
||||
*/
|
||||
public static function nextPossiblePauseDate(UserAbo $user_abo): ?Carbon
|
||||
{
|
||||
$lastPauseDate = $user_abo->getRawOriginal('last_pause_date');
|
||||
|
||||
if (! $lastPauseDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nextPossible = Carbon::parse($lastPauseDate)->startOfDay()->addMonthsNoOverflow(self::getPauseIntervalMonths());
|
||||
|
||||
return $nextPossible->lte(Carbon::today()) ? null : $nextPossible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob das Bearbeiten des Abos (Liefertag/Intervall) aktuell durch die
|
||||
* Sperrfrist vor der Ausführung blockiert ist. Admins umgehen die Sperre.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ class BackofficeDashboardService
|
|||
->first();
|
||||
|
||||
if ($snapshot) {
|
||||
return $this->withMeta($snapshot->payload, 'snapshot', $snapshot->calculated_at?->format('d.m.Y H:i'));
|
||||
return $this->withMeta(
|
||||
$this->overlayCurrentAboStock($user, $snapshot->payload),
|
||||
'snapshot',
|
||||
$snapshot->calculated_at?->format('d.m.Y H:i')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +93,10 @@ class BackofficeDashboardService
|
|||
$totals = $this->addToTotals($totals, $row);
|
||||
}
|
||||
|
||||
// 1000-Punkte-Shop: der VIP selbst zählt mit (Kundenwunsch), obwohl er in
|
||||
// keiner Linie steckt — nur diese Kennzahl bekommt ihn zusätzlich.
|
||||
$totals['shop_1000'] += $this->shop1000Count([(int) $user->id], $month, $year);
|
||||
|
||||
$totals['label'] = 'Summe';
|
||||
|
||||
return [
|
||||
|
|
@ -97,11 +105,91 @@ class BackofficeDashboardService
|
|||
'metric_labels' => $this->metricLabels(),
|
||||
'lines' => $lines,
|
||||
'totals' => $totals,
|
||||
'new_customer_abos' => $this->newCustomerAboRanking($lineBuckets, $month, $year),
|
||||
'non_payline_points' => $this->nonPaylinePointsSum($totals['user_ids'], $month, $year),
|
||||
'new_customer_abos' => $this->newCustomerAboRanking($user, $lineBuckets, $month, $year),
|
||||
'leader_abos' => $this->leaderAboOverview($lineBuckets),
|
||||
'top_customer_abos' => $this->topCustomerAboRanking($user, $lineBuckets, $month, $year),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Überlagert einen Snapshot-Payload mit den tagesaktuellen Abo-Bestandszahlen
|
||||
* (Kundenentscheid F7): Teamabos/Teamkundenabos je Linie + Summe sowie die
|
||||
* Führungskräfte- und Top-10-Listen sind Bestands-Kennzahlen ohne Monatsbezug
|
||||
* und werden live nachgerechnet. Monatsbezogene Werte (Punkte, Umsätze,
|
||||
* Neupartner, "+N neu", neue Kundenabos) bleiben eingefroren.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function overlayCurrentAboStock(User $user, array $payload): array
|
||||
{
|
||||
$lineBuckets = $this->lineBuckets($user->id);
|
||||
|
||||
$totalPartnerAbos = 0;
|
||||
$totalCustomerAbos = 0;
|
||||
|
||||
$snapshotLines = collect($payload['lines'] ?? [])
|
||||
->map(fn ($row, $key) => (int) ($row['line'] ?? $key))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
foreach ($payload['lines'] ?? [] as $key => $row) {
|
||||
$line = (int) ($row['line'] ?? $key);
|
||||
$userIds = ($lineBuckets[$line] ?? collect())->pluck('id')->map(fn ($id) => (int) $id)->all();
|
||||
|
||||
$partnerAbos = $userIds === [] ? 0 : $this->activeAboQuery()->whereIn('user_id', $userIds)->where('is_for', 'me')->count();
|
||||
$customerAbos = $userIds === [] ? 0 : $this->activeAboQuery()->whereIn('member_id', $userIds)->where('is_for', 'ot')->count();
|
||||
|
||||
$payload['lines'][$key]['team_partner_abos'] = $partnerAbos;
|
||||
$payload['lines'][$key]['team_customer_abos'] = $customerAbos;
|
||||
|
||||
$totalPartnerAbos += $partnerAbos;
|
||||
$totalCustomerAbos += $customerAbos;
|
||||
}
|
||||
|
||||
// Linien, die erst nach dem Snapshot entstanden sind (Team ist heute tiefer),
|
||||
// tragen ebenfalls aktuellen Abo-Bestand: als neue Zeile anhängen, Monatswerte 0.
|
||||
foreach ($lineBuckets as $line => $users) {
|
||||
if (in_array((int) $line, $snapshotLines, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userIds = $users->pluck('id')->map(fn ($id) => (int) $id)->all();
|
||||
if ($userIds === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$partnerAbos = $this->activeAboQuery()->whereIn('user_id', $userIds)->where('is_for', 'me')->count();
|
||||
$customerAbos = $this->activeAboQuery()->whereIn('member_id', $userIds)->where('is_for', 'ot')->count();
|
||||
|
||||
if ($partnerAbos === 0 && $customerAbos === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $this->emptyLine((int) $line, $userIds);
|
||||
$row['team_partner_abos'] = $partnerAbos;
|
||||
$row['team_customer_abos'] = $customerAbos;
|
||||
$payload['lines'][(int) $line] = $row;
|
||||
|
||||
$totalPartnerAbos += $partnerAbos;
|
||||
$totalCustomerAbos += $customerAbos;
|
||||
}
|
||||
|
||||
$payload['lines'] = collect($payload['lines'] ?? [])->sortBy('line')->all();
|
||||
|
||||
if (isset($payload['totals'])) {
|
||||
$payload['totals']['team_partner_abos'] = $totalPartnerAbos;
|
||||
$payload['totals']['team_customer_abos'] = $totalCustomerAbos;
|
||||
}
|
||||
|
||||
$payload['leader_abos'] = $this->leaderAboOverview($lineBuckets);
|
||||
$payload['top_customer_abos'] = $this->topCustomerAboRanking($user, $lineBuckets, (int) ($payload['month'] ?? now()->month), (int) ($payload['year'] ?? now()->year));
|
||||
$payload['abo_stock_live'] = true;
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overview
|
||||
* @return array<string, mixed>
|
||||
|
|
@ -123,7 +211,7 @@ class BackofficeDashboardService
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<int, \Illuminate\Support\Collection<int, \App\User>>
|
||||
* @return array<int, Collection<int, User>>
|
||||
*/
|
||||
public function lineBuckets(int $rootUserId): array
|
||||
{
|
||||
|
|
@ -206,7 +294,7 @@ class BackofficeDashboardService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
* @param Collection<int, User> $users
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildLineRow(int $line, Collection $users, int $month, int $year): array
|
||||
|
|
@ -286,7 +374,7 @@ class BackofficeDashboardService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
* @param Collection<int, User> $users
|
||||
*/
|
||||
private function activeConsultants(Collection $users): int
|
||||
{
|
||||
|
|
@ -296,7 +384,7 @@ class BackofficeDashboardService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
* @param Collection<int, User> $users
|
||||
*/
|
||||
private function newPartners(Collection $users, int $month, int $year): int
|
||||
{
|
||||
|
|
@ -376,6 +464,27 @@ class BackofficeDashboardService
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Summe der Punkte des Teams, die nicht in die Payline fließen (nur-KP-Einträge
|
||||
* wie Neupartner-Einschreibungspunkte) — erklärt die Differenz zwischen den
|
||||
* Gesamtpunkten hier und der PP-Summe der Struktur-/Provisionsansicht.
|
||||
*
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
private function nonPaylinePointsSum(array $userIds, int $month, int $year): float
|
||||
{
|
||||
if ($userIds === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) UserSalesVolume::query()
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->selectRaw(SalesVolumePoints::nonPaylinePoints().' as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzahl der Neupartner (aktiver Account, im Monat aktiv geworden).
|
||||
* Konsistent mit der Drilldown-Liste {@see BackofficeDrilldownService::newPartnerRows()}.
|
||||
|
|
@ -404,14 +513,15 @@ class BackofficeDashboardService
|
|||
* Ranking: Wer im Team hat im Monat wie viele neue Kundenabos abgeschlossen?
|
||||
* Sortiert nach Anzahl absteigend (nicht nach Linie), Linie wird je Berater mitgeliefert,
|
||||
* dazu der aktuelle Gesamtbestand an Kundenabos des Beraters.
|
||||
* Der Statistik-Nutzer selbst zählt mit (Kundenwunsch, Linie 0 / "ich").
|
||||
* Zählweise identisch zu team_customer_abos_new {@see newAboCount()}.
|
||||
*
|
||||
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
|
||||
* @return array<int, array{user_id: int, name: string, career_level: string, line: int, abo_count: int, total_abo_count: int}>
|
||||
* @param array<int, Collection<int, User>> $lineBuckets
|
||||
* @return array<int, array{user_id: int, name: string, career_level: string, line: int, is_self: bool, abo_count: int, total_abo_count: int}>
|
||||
*/
|
||||
private function newCustomerAboRanking(array $lineBuckets, int $month, int $year): array
|
||||
private function newCustomerAboRanking(User $rootUser, array $lineBuckets, int $month, int $year): array
|
||||
{
|
||||
$usersByLine = $this->usersWithLine($lineBuckets);
|
||||
$usersByLine = $this->usersWithLine($lineBuckets, $rootUser);
|
||||
|
||||
if ($usersByLine === []) {
|
||||
return [];
|
||||
|
|
@ -437,6 +547,7 @@ class BackofficeDashboardService
|
|||
'name' => $this->userName($entry['user']),
|
||||
'career_level' => $this->careerLevel($entry['user']),
|
||||
'line' => $entry['line'],
|
||||
'is_self' => $entry['line'] === 0,
|
||||
'abo_count' => (int) $counts->get($userId),
|
||||
'total_abo_count' => (int) $totalCounts->get($userId, 0),
|
||||
])
|
||||
|
|
@ -447,10 +558,21 @@ class BackofficeDashboardService
|
|||
|
||||
/**
|
||||
* Abos der Führungskräfte (ab Stufe Vertriebsleiter) im Team: Gesamtzahlen plus
|
||||
* Liste aller Führungskräfte inklusive Linie, eigener Abos und Kundenabos.
|
||||
* Liste aller Führungskräfte inklusive Linie, eigener Abos, Kundenabos sowie
|
||||
* Team-Abos und Team-Kundenabos ihres Teilbaums und der Gesamtsumme aller
|
||||
* vier Kennzahlen (Kundenwunsch B7).
|
||||
*
|
||||
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
|
||||
* @return array{level_name: string, total: int, total_own: int, total_customer: int, rows: array<int, array{user_id: int, name: string, career_level: string, line: int, own_abo_count: int, customer_abo_count: int}>}
|
||||
* Zwei Team-Sichten je Führungskraft (Kundenentscheid 2026-07-24, revidiert F9):
|
||||
* - team_abo_count / team_customer_abo_count ("direkt"): Teilbaum wird an der
|
||||
* nächsten Führungskraft abgeschnitten — überlappungsfrei, die Kopf-Summen
|
||||
* (total_*) bleiben dadurch echte Gesamtwerte ohne Doppelzählung.
|
||||
* - team_abo_count_total / team_customer_abo_count_total ("gesamt"): komplette
|
||||
* Downline der Führungskraft; verschachtelte Führungskräfte zählen hier
|
||||
* mehrfach, deshalb werden diese Spalten nicht aufsummiert.
|
||||
* total_abo_count_full = eigene + Kundenabos + Team gesamt (Sortierung).
|
||||
*
|
||||
* @param array<int, Collection<int, User>> $lineBuckets
|
||||
* @return array{level_name: string, total: int, total_own: int, total_customer: int, total_team: int, total_team_customer: int, rows: array<int, array{user_id: int, name: string, career_level: string, line: int, own_abo_count: int, customer_abo_count: int, team_abo_count: int, team_customer_abo_count: int, team_abo_count_total: int, team_customer_abo_count_total: int, total_abo_count: int, total_abo_count_full: int}>}
|
||||
*/
|
||||
private function leaderAboOverview(array $lineBuckets): array
|
||||
{
|
||||
|
|
@ -458,54 +580,227 @@ class BackofficeDashboardService
|
|||
->where('name', self::LEADER_LEVEL_NAME)
|
||||
->value('pos');
|
||||
|
||||
$leadersByLine = $leaderMinPos === null ? [] : collect($this->usersWithLine($lineBuckets))
|
||||
->filter(fn (array $entry) => ($entry['user']->user_level?->pos ?? 0) >= $leaderMinPos)
|
||||
$usersWithLine = $this->usersWithLine($lineBuckets);
|
||||
$isLeader = fn (User $user): bool => $leaderMinPos !== null && ($user->user_level?->pos ?? 0) >= $leaderMinPos;
|
||||
|
||||
$leadersByLine = $leaderMinPos === null ? [] : collect($usersWithLine)
|
||||
->filter(fn (array $entry) => $isLeader($entry['user']))
|
||||
->all();
|
||||
|
||||
if ($leadersByLine === []) {
|
||||
return ['level_name' => self::LEADER_LEVEL_NAME, 'total' => 0, 'total_own' => 0, 'total_customer' => 0, 'rows' => []];
|
||||
return ['level_name' => self::LEADER_LEVEL_NAME, 'total' => 0, 'total_own' => 0, 'total_customer' => 0, 'total_team' => 0, 'total_team_customer' => 0, 'rows' => []];
|
||||
}
|
||||
|
||||
$leaderIds = array_keys($leadersByLine);
|
||||
$teamIdsByLeader = $this->teamIdsPerLeader($usersWithLine, $leaderIds, $isLeader);
|
||||
$fullTeamIdsByLeader = $this->teamIdsPerLeader($usersWithLine, $leaderIds, fn (User $user): bool => false);
|
||||
$allCountIds = array_values(array_unique(array_merge($leaderIds, ...array_values($fullTeamIdsByLeader))));
|
||||
|
||||
$ownCounts = $this->activeAboQuery()
|
||||
->whereIn('user_id', $leaderIds)
|
||||
->whereIn('user_id', $allCountIds)
|
||||
->where('is_for', 'me')
|
||||
->selectRaw('user_id, COUNT(*) as abo_count')
|
||||
->groupBy('user_id')
|
||||
->pluck('abo_count', 'user_id');
|
||||
|
||||
$customerCounts = $this->customerAboCounts($leaderIds);
|
||||
$customerCounts = $this->customerAboCounts($allCountIds);
|
||||
|
||||
$rows = collect($leadersByLine)
|
||||
->map(function (array $entry, int $userId) use ($ownCounts, $customerCounts, $teamIdsByLeader, $fullTeamIdsByLeader) {
|
||||
$teamIds = $teamIdsByLeader[$userId] ?? [];
|
||||
$fullTeamIds = $fullTeamIdsByLeader[$userId] ?? [];
|
||||
$own = (int) $ownCounts->get($userId, 0);
|
||||
$customer = (int) $customerCounts->get($userId, 0);
|
||||
$team = (int) collect($teamIds)->sum(fn (int $teamUserId) => (int) $ownCounts->get($teamUserId, 0));
|
||||
$teamCustomer = (int) collect($teamIds)->sum(fn (int $teamUserId) => (int) $customerCounts->get($teamUserId, 0));
|
||||
$teamTotal = (int) collect($fullTeamIds)->sum(fn (int $teamUserId) => (int) $ownCounts->get($teamUserId, 0));
|
||||
$teamCustomerTotal = (int) collect($fullTeamIds)->sum(fn (int $teamUserId) => (int) $customerCounts->get($teamUserId, 0));
|
||||
|
||||
return [
|
||||
'user_id' => $userId,
|
||||
'name' => $this->userName($entry['user']),
|
||||
'career_level' => $this->careerLevel($entry['user']),
|
||||
'line' => $entry['line'],
|
||||
'own_abo_count' => $own,
|
||||
'customer_abo_count' => $customer,
|
||||
'team_abo_count' => $team,
|
||||
'team_customer_abo_count' => $teamCustomer,
|
||||
'team_abo_count_total' => $teamTotal,
|
||||
'team_customer_abo_count_total' => $teamCustomerTotal,
|
||||
'total_abo_count' => $own + $customer + $team + $teamCustomer,
|
||||
'total_abo_count_full' => $own + $customer + $teamTotal + $teamCustomerTotal,
|
||||
];
|
||||
})
|
||||
->sortByDesc('total_abo_count_full')
|
||||
->values();
|
||||
|
||||
$totalOwn = (int) $rows->sum('own_abo_count');
|
||||
$totalCustomer = (int) $rows->sum('customer_abo_count');
|
||||
$totalTeam = (int) $rows->sum('team_abo_count');
|
||||
$totalTeamCustomer = (int) $rows->sum('team_customer_abo_count');
|
||||
|
||||
return [
|
||||
'level_name' => self::LEADER_LEVEL_NAME,
|
||||
'total' => $totalOwn + $totalCustomer + $totalTeam + $totalTeamCustomer,
|
||||
'total_own' => $totalOwn,
|
||||
'total_customer' => $totalCustomer,
|
||||
'total_team' => $totalTeam,
|
||||
'total_team_customer' => $totalTeamCustomer,
|
||||
'rows' => $rows->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilbaum-User-IDs je Führungskraft aus der gemeinsamen Sponsor-Map
|
||||
* (ein Durchlauf über die bereits geladene Downline, keine Abfragen pro
|
||||
* Führungskraft). Der Abstieg stoppt an jedem Kind, für das $isLeader true
|
||||
* liefert (dieses und sein Team werden übersprungen); mit einem immer-false
|
||||
* Callable ergibt sich die komplette Downline.
|
||||
*
|
||||
* @param array<int, array{user: User, line: int}> $usersWithLine
|
||||
* @param int[] $leaderIds
|
||||
* @return array<int, int[]>
|
||||
*/
|
||||
private function teamIdsPerLeader(array $usersWithLine, array $leaderIds, callable $isLeader): array
|
||||
{
|
||||
$childrenBySponsor = [];
|
||||
|
||||
foreach ($usersWithLine as $entry) {
|
||||
$childrenBySponsor[(int) $entry['user']->m_sponsor][] = $entry['user'];
|
||||
}
|
||||
|
||||
$teamIdsByLeader = [];
|
||||
|
||||
foreach ($leaderIds as $leaderId) {
|
||||
$teamIds = [];
|
||||
$queue = $childrenBySponsor[$leaderId] ?? [];
|
||||
|
||||
while ($queue !== []) {
|
||||
$child = array_shift($queue);
|
||||
|
||||
if ($isLeader($child)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$teamIds[] = (int) $child->id;
|
||||
|
||||
foreach ($childrenBySponsor[(int) $child->id] ?? [] as $grandChild) {
|
||||
$queue[] = $grandChild;
|
||||
}
|
||||
}
|
||||
|
||||
$teamIdsByLeader[$leaderId] = $teamIds;
|
||||
}
|
||||
|
||||
return $teamIdsByLeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-10-Ranglisten der Kundenabos im eigenen Team (Kundenwunsch B5/B6,
|
||||
* "unabhängig vom Incentive" = aktueller Gesamtbestand aktiver Kundenabos):
|
||||
*
|
||||
* - by_count: nach Anzahl aktiver Kundenabos je Berater
|
||||
* - by_points: nach Umsatz mit Ist (im gewählten Monat abgerechnete
|
||||
* Kundenabo-Punkte aus user_sales_volumes, Hauptwert und Sortierung —
|
||||
* Kundenentscheid 2026-07-24) und Soll (Abo-Punkte-Bestand, Produktpunkte
|
||||
* × Menge der laufenden Kundenabos) als Zweitwert
|
||||
*
|
||||
* Der Statistik-Nutzer selbst zählt in beiden Listen mit (Linie 0 / "ich").
|
||||
*
|
||||
* @param array<int, Collection<int, User>> $lineBuckets
|
||||
* @return array{by_count: array<int, array<string, mixed>>, by_points: array<int, array<string, mixed>>}
|
||||
*/
|
||||
private function topCustomerAboRanking(User $rootUser, array $lineBuckets, int $month, int $year): array
|
||||
{
|
||||
$usersWithLine = $this->usersWithLine($lineBuckets, $rootUser);
|
||||
|
||||
if ($usersWithLine === []) {
|
||||
return ['by_count' => [], 'by_points' => []];
|
||||
}
|
||||
|
||||
$memberIds = array_keys($usersWithLine);
|
||||
$aboCounts = $this->customerAboCounts($memberIds);
|
||||
$targetPoints = $this->customerAboTargetPoints($memberIds);
|
||||
$actualPoints = $this->customerAboActualPoints($memberIds, $month, $year);
|
||||
|
||||
$rows = collect($usersWithLine)
|
||||
->map(fn (array $entry, int $userId) => [
|
||||
'user_id' => $userId,
|
||||
'name' => $this->userName($entry['user']),
|
||||
'career_level' => $this->careerLevel($entry['user']),
|
||||
'line' => $entry['line'],
|
||||
'own_abo_count' => (int) $ownCounts->get($userId, 0),
|
||||
'customer_abo_count' => (int) $customerCounts->get($userId, 0),
|
||||
])
|
||||
->sortByDesc(fn (array $row) => $row['own_abo_count'] + $row['customer_abo_count'])
|
||||
->values();
|
||||
|
||||
$totalOwn = (int) $rows->sum('own_abo_count');
|
||||
$totalCustomer = (int) $rows->sum('customer_abo_count');
|
||||
'is_self' => $entry['line'] === 0,
|
||||
'abo_count' => (int) $aboCounts->get($userId, 0),
|
||||
'points_target' => (float) $targetPoints->get($userId, 0.0),
|
||||
'points_actual' => (float) $actualPoints->get($userId, 0.0),
|
||||
]);
|
||||
|
||||
return [
|
||||
'level_name' => self::LEADER_LEVEL_NAME,
|
||||
'total' => $totalOwn + $totalCustomer,
|
||||
'total_own' => $totalOwn,
|
||||
'total_customer' => $totalCustomer,
|
||||
'rows' => $rows->all(),
|
||||
'by_count' => $rows
|
||||
->filter(fn (array $row) => $row['abo_count'] > 0)
|
||||
->sortByDesc('abo_count')
|
||||
->take(10)
|
||||
->values()
|
||||
->all(),
|
||||
'by_points' => $rows
|
||||
->filter(fn (array $row) => $row['points_target'] > 0 || $row['points_actual'] > 0)
|
||||
->sortByDesc('points_actual')
|
||||
->take(10)
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Soll-Punkte je Berater: Abo-Punkte-Bestand der aktiven Kundenabos
|
||||
* (Produktpunkte × Menge, nur Hauptartikel comp = 0 — gleiche Logik wie
|
||||
* {@see UserAbo::getTotalPoints()}).
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
* @return Collection<int, float>
|
||||
*/
|
||||
private function customerAboTargetPoints(array $memberIds): Collection
|
||||
{
|
||||
return UserAbo::query()
|
||||
->join('user_abo_items', 'user_abo_items.user_abo_id', '=', 'user_abos.id')
|
||||
->join('products', 'products.id', '=', 'user_abo_items.product_id')
|
||||
->whereNull('products.deleted_at')
|
||||
->where('user_abos.active', true)
|
||||
->whereNotIn('user_abos.status', [4, 5, 6])
|
||||
->whereIn('user_abos.member_id', $memberIds)
|
||||
->where('user_abos.is_for', 'ot')
|
||||
->where('user_abo_items.comp', 0)
|
||||
->selectRaw('user_abos.member_id, COALESCE(SUM(products.points * user_abo_items.qty), 0) as points')
|
||||
->groupBy('user_abos.member_id')
|
||||
->pluck('points', 'member_id')
|
||||
->map(fn ($points) => (float) $points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist-Punkte je Berater: im Monat tatsächlich abgerechnete Kundenabo-Punkte.
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
* @return Collection<int, float>
|
||||
*/
|
||||
private function customerAboActualPoints(array $memberIds, int $month, int $year): Collection
|
||||
{
|
||||
return UserSalesVolume::query()
|
||||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->whereIn('user_id', $memberIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->select('user_sales_volumes.user_id')
|
||||
->selectRaw(SalesVolumePoints::customerAboPoints().' as points')
|
||||
->groupBy('user_sales_volumes.user_id')
|
||||
->pluck('points', 'user_id')
|
||||
->map(fn ($points) => (float) $points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktueller Bestand aktiver Kundenabos je Berater (member_id).
|
||||
*
|
||||
* @param int[] $memberIds
|
||||
* @return \Illuminate\Support\Collection<int, int>
|
||||
* @return Collection<int, int>
|
||||
*/
|
||||
private function customerAboCounts(array $memberIds): Collection
|
||||
{
|
||||
|
|
@ -522,13 +817,20 @@ class BackofficeDashboardService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
|
||||
* @return array<int, array{user: \App\User, line: int}>
|
||||
* Downline-User mit ihrer Linie; optional inklusive des Statistik-Nutzers
|
||||
* selbst als Linie 0 (er steckt in keinem lineBucket, {@see lineBuckets()}).
|
||||
*
|
||||
* @param array<int, Collection<int, User>> $lineBuckets
|
||||
* @return array<int, array{user: User, line: int}>
|
||||
*/
|
||||
private function usersWithLine(array $lineBuckets): array
|
||||
private function usersWithLine(array $lineBuckets, ?User $rootUser = null): array
|
||||
{
|
||||
$usersByLine = [];
|
||||
|
||||
if ($rootUser !== null) {
|
||||
$usersByLine[(int) $rootUser->id] = ['user' => $rootUser, 'line' => 0];
|
||||
}
|
||||
|
||||
foreach ($lineBuckets as $line => $users) {
|
||||
foreach ($users as $user) {
|
||||
$usersByLine[(int) $user->id] = ['user' => $user, 'line' => $line];
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ use App\Models\UserSalesVolume;
|
|||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BackofficeDrilldownService
|
||||
{
|
||||
public function __construct(private BackofficeDashboardService $dashboardService) {}
|
||||
|
||||
/**
|
||||
* @return array{metric: string, metric_label: string, line: int, line_label: string, month: int, year: int, rows: array<int, array<string, mixed>>, summary: array<string, mixed>}
|
||||
* @return array{metric: string, metric_label: string, only_new: bool, line: int, line_label: string, month: int, year: int, rows: array<int, array<string, mixed>>, summary: array<string, mixed>}
|
||||
*/
|
||||
public function details(User $viewer, int $line, string $metric, int $month, int $year): array
|
||||
public function details(User $viewer, int $line, string $metric, int $month, int $year, bool $onlyNew = false): array
|
||||
{
|
||||
$metricLabels = $this->dashboardService->metricLabels();
|
||||
|
||||
|
|
@ -23,19 +24,31 @@ class BackofficeDrilldownService
|
|||
abort(404);
|
||||
}
|
||||
|
||||
$onlyNew = $onlyNew && in_array($metric, ['team_partner_abos', 'team_customer_abos'], true);
|
||||
|
||||
$userIds = $this->dashboardService->lineUserIds($viewer->id, $line);
|
||||
|
||||
if ($metric === 'shop_1000' && $line === 0) {
|
||||
$userIds[] = (int) $viewer->id;
|
||||
}
|
||||
|
||||
$rows = match ($metric) {
|
||||
'consultants' => $this->consultantRows($userIds),
|
||||
'new_partners' => $this->newPartnerRows($userIds, $month, $year),
|
||||
'team_partner_abos' => $this->partnerAboRows($userIds, $month, $year),
|
||||
'team_customer_abos' => $this->customerAboRows($userIds, $month, $year),
|
||||
'team_partner_abos' => $this->partnerAboRows($userIds, $month, $year, $onlyNew),
|
||||
'team_customer_abos' => $this->customerAboRows($userIds, $month, $year, $onlyNew),
|
||||
'own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000' => $this->pointsRows($userIds, $month, $year, $metric),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if ($metric === 'shop_1000') {
|
||||
$rows = $this->markViewerRow($rows, (int) $viewer->id);
|
||||
}
|
||||
|
||||
return [
|
||||
'metric' => $metric,
|
||||
'metric_label' => $metricLabels[$metric],
|
||||
'metric_label' => $metricLabels[$metric].($onlyNew ? ' – nur neue' : ''),
|
||||
'only_new' => $onlyNew,
|
||||
'line' => $line,
|
||||
'line_label' => $line > 0 ? 'Linie '.$line : 'Alle Linien',
|
||||
'month' => $month,
|
||||
|
|
@ -46,8 +59,26 @@ class BackofficeDrilldownService
|
|||
}
|
||||
|
||||
/**
|
||||
* Kennzeichnet die eigene Zeile des Betrachters (der VIP zählt beim
|
||||
* 1000-Punkte-Shop selbst mit und soll in der Liste erkennbar sein).
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array{count: int, abo_count: int, points: float, own_points: float, external_points: float, customer_abo_points: float, customer_single_order_points: float, customer_other_points: float, total_points: float, deliveries: int}
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function markViewerRow(array $rows, int $viewerId): array
|
||||
{
|
||||
return array_map(function (array $row) use ($viewerId): array {
|
||||
if ((int) ($row['user_id'] ?? 0) === $viewerId) {
|
||||
$row['name'] .= ' (ich)';
|
||||
}
|
||||
|
||||
return $row;
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array{count: int, abo_count: int, points: float, customer_points: float, own_points: float, external_points: float, customer_abo_points: float, customer_single_order_points: float, customer_other_points: float, total_points: float, deliveries: int}
|
||||
*/
|
||||
private function summary(array $rows): array
|
||||
{
|
||||
|
|
@ -55,6 +86,7 @@ class BackofficeDrilldownService
|
|||
'count' => count($rows),
|
||||
'abo_count' => (int) collect($rows)->sum('abo_count'),
|
||||
'points' => (float) collect($rows)->sum('points'),
|
||||
'customer_points' => (float) collect($rows)->sum('customer_points'),
|
||||
'own_points' => (float) collect($rows)->sum('own_points'),
|
||||
'external_points' => (float) collect($rows)->sum('external_points'),
|
||||
'customer_abo_points' => (float) collect($rows)->sum('customer_abo_points'),
|
||||
|
|
@ -102,7 +134,7 @@ class BackofficeDrilldownService
|
|||
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
|
||||
return User::query()
|
||||
$partners = User::query()
|
||||
->with(['account', 'user_level'])
|
||||
->whereIn('id', $userIds)
|
||||
->whereNotNull('m_level')
|
||||
|
|
@ -111,12 +143,25 @@ class BackofficeDrilldownService
|
|||
->orderBy('active_date')
|
||||
->get()
|
||||
->filter(fn (User $user) => Carbon::parse($user->payment_account)->isFuture())
|
||||
->values();
|
||||
|
||||
$sponsors = User::query()
|
||||
->with('account')
|
||||
->whereIn('id', $partners->pluck('m_sponsor')->filter()->unique())
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$customerPoints = $this->externalPointsByUser($partners->pluck('id')->map(fn ($id) => (int) $id)->all(), $month, $year);
|
||||
|
||||
return $partners
|
||||
->map(fn (User $user) => [
|
||||
'type' => 'user',
|
||||
'user_id' => $user->id,
|
||||
'name' => $this->userName($user),
|
||||
'email' => $user->email,
|
||||
'career_level' => $this->careerLevel($user),
|
||||
'sponsor' => $user->m_sponsor && $sponsors->has((int) $user->m_sponsor) ? $this->userName($sponsors->get((int) $user->m_sponsor)) : '-',
|
||||
'customer_points' => (float) $customerPoints->get((int) $user->id, 0.0),
|
||||
'active_date' => $this->formatDate($user->active_date),
|
||||
'payment_account' => $this->formatDate($user->payment_account),
|
||||
])
|
||||
|
|
@ -124,16 +169,41 @@ class BackofficeDrilldownService
|
|||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Externe Kundenpunkte (Shop + Abo) je User im gewählten Monat — tagesaktuell
|
||||
* aus user_sales_volumes summiert (gleiche Status-Logik wie die Übersicht).
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @return Collection<int, float>
|
||||
*/
|
||||
private function externalPointsByUser(array $userIds, int $month, int $year): Collection
|
||||
{
|
||||
if ($userIds === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return UserSalesVolume::query()
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->select('user_id')
|
||||
->selectRaw(SalesVolumePoints::externalPoints().' as external_points')
|
||||
->groupBy('user_id')
|
||||
->pluck('external_points', 'user_id')
|
||||
->map(fn ($points) => (float) $points);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $userIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function partnerAboRows(array $userIds, int $month, int $year): array
|
||||
private function partnerAboRows(array $userIds, int $month, int $year, bool $onlyNew = false): array
|
||||
{
|
||||
return $this->activeAboQuery()
|
||||
->with(['user.account', 'user.user_level', 'user_abo_items.product', 'user_abo_orders.shopping_order.shopping_payments.payment_transactions'])
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('is_for', 'me')
|
||||
->when($onlyNew, fn (Builder $query) => $this->whereNewInMonth($query, $month, $year))
|
||||
->orderBy('next_date')
|
||||
->get()
|
||||
->map(fn (UserAbo $abo) => [
|
||||
|
|
@ -165,12 +235,13 @@ class BackofficeDrilldownService
|
|||
* @param int[] $userIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function customerAboRows(array $userIds, int $month, int $year): array
|
||||
private function customerAboRows(array $userIds, int $month, int $year, bool $onlyNew = false): array
|
||||
{
|
||||
return $this->activeAboQuery()
|
||||
->with(['member.account', 'member.user_level', 'user_abo_items.product'])
|
||||
->whereIn('member_id', $userIds)
|
||||
->where('is_for', 'ot')
|
||||
->when($onlyNew, fn (Builder $query) => $this->whereNewInMonth($query, $month, $year))
|
||||
->orderBy('member_id')
|
||||
->orderBy('next_date')
|
||||
->get()
|
||||
|
|
@ -279,6 +350,17 @@ class BackofficeDrilldownService
|
|||
->whereNotIn('status', [4, 5, 6]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Abos, die im gewählten Monat gestartet sind (gleiche Zählweise wie
|
||||
* die "+N neu"-Badges, {@see BackofficeDashboardService::newAboCount()}).
|
||||
*/
|
||||
private function whereNewInMonth(Builder $query, int $month, int $year): Builder
|
||||
{
|
||||
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
|
||||
return $query->whereBetween('start_date', [$startDate, $startDate->copy()->endOfMonth()]);
|
||||
}
|
||||
|
||||
private function userName(User $user): string
|
||||
{
|
||||
$name = trim(($user->account?->first_name ?? '').' '.($user->account?->last_name ?? ''));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Services\Backoffice;
|
||||
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
|
||||
/**
|
||||
* Liefert SQL-Aggregat-Ausdrücke für die monatliche Punkte-/Umsatzauswertung
|
||||
* der Backoffice-Statistik.
|
||||
|
|
@ -11,7 +13,7 @@ namespace App\Services\Backoffice;
|
|||
* kumulative Monats-Laufstand zum Zeitpunkt der Bestellung – KEINE Einzelbeträge.
|
||||
* Eine SUM() über diese Spalten zählt mehrfach. Korrekt wird je Zeile die
|
||||
* `points`/`total_net`-Spalte summiert und nach `status` gefiltert – analog zu
|
||||
* {@see \App\Services\BusinessPlan\SalesPointsVolume::reCalculateSalesPointsVolume()}.
|
||||
* {@see SalesPointsVolume::reCalculateSalesPointsVolume()}.
|
||||
*
|
||||
* Status-Zuordnung:
|
||||
* - Eigenpunkte (KP): status 1, 4, 5, 6
|
||||
|
|
@ -59,6 +61,26 @@ class SalesVolumePoints
|
|||
return self::ownPoints().' + '.self::externalPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Punkte aus Abo-Bestellungen (eigene wie Kundenabos) — alle zählbaren Status.
|
||||
* Benötigt den Join auf shopping_orders.
|
||||
*/
|
||||
public static function aboPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::TURNOVER_PREDICATE.' AND shopping_orders.is_abo = 1', 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Punkte, die nur als Karrierepunkte (KP) des Beraters zählen und NICHT als
|
||||
* Teampunkte in die Payline fließen (status_points = 2, z. B.
|
||||
* Neupartner-Einschreibungspunkte, manuelle Gutschriften) — vgl.
|
||||
* {@see SalesPointsVolume::add_KP_TP_Points()}.
|
||||
*/
|
||||
public static function nonPaylinePoints(): string
|
||||
{
|
||||
return self::sumWhere(self::OWN_PREDICATE.' AND user_sales_volumes.status_points = 2', 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function turnoverNet(): string
|
||||
{
|
||||
return self::sumWhere(self::TURNOVER_PREDICATE, 'user_sales_volumes.total_net');
|
||||
|
|
|
|||
145
app/Services/BusinessPlan/LiveCommissionService.php
Normal file
145
app/Services/BusinessPlan/LiveCommissionService.php
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\Models\UserBusiness;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* Gemeinsamer Live-Lauf für die "Stand heute"-Kennzahlen (Konzept B1/B8/B10):
|
||||
* EIN TreeCalcBotOptimized-Lauf pro User/Monat liefert Payline-Punkte,
|
||||
* Provisions-Bestandteile und die Payline-Tiefe, 6 h gecacht (F5).
|
||||
*
|
||||
* - Liegt für den Monat bereits ein UserBusiness-Datensatz vor (Monats-Batch
|
||||
* `business:store-optimized` gelaufen), gelten dessen Werte als final.
|
||||
* - Sonst rechnet TreeCalcBotOptimized live — dieselbe Engine wie der
|
||||
* Monats-Batch (inkl. GrowthBonusCalculator und korrigierter Level-Auswahl),
|
||||
* damit Live- und Finalwerte nicht systematisch auseinanderlaufen.
|
||||
* - Fehlgeschlagene Läufe werden NICHT gecacht, damit ein transienter Fehler
|
||||
* nicht 6 h lang Nullwerte anzeigt (`failed` markiert diesen Fall).
|
||||
*/
|
||||
class LiveCommissionService
|
||||
{
|
||||
private const CACHE_HOURS = 6;
|
||||
|
||||
/**
|
||||
* @return array{payline_points: float, paylines: int, pp: float, shop: float, growth: float, is_live: bool, failed: bool}
|
||||
*/
|
||||
public function snapshot(User $user, int $month, int $year): array
|
||||
{
|
||||
$stored = UserBusiness::where('user_id', $user->id)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if ($stored) {
|
||||
return $this->buildSnapshot(
|
||||
(float) ($stored->payline_points ?? 0),
|
||||
$this->paylinesFromQualLevel($stored->qual_user_level),
|
||||
(float) ($stored->commission_pp_total ?? 0),
|
||||
(float) ($stored->commission_shop_sales ?? 0),
|
||||
(float) ($stored->commission_growth_total ?? 0),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
$cacheKey = "live_business_{$user->id}_{$month}_{$year}";
|
||||
$cached = Cache::get($cacheKey);
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$snapshot = $this->calculateLive($user, $month, $year);
|
||||
if ($snapshot !== null) {
|
||||
Cache::put($cacheKey, $snapshot, now()->addHours(self::CACHE_HOURS));
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
return $this->buildSnapshot(0.0, 0, 0.0, 0.0, 0.0, true, failed: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision "Stand heute" (B1) — alle Bestandteile plus Summe.
|
||||
*
|
||||
* @return array{total: float, pp: float, shop: float, growth: float, is_live: bool}
|
||||
*/
|
||||
public function commission(User $user, int $month, int $year): array
|
||||
{
|
||||
$snapshot = $this->snapshot($user, $month, $year);
|
||||
|
||||
return [
|
||||
'total' => round($snapshot['pp'] + $snapshot['shop'] + $snapshot['growth'], 2),
|
||||
'pp' => $snapshot['pp'],
|
||||
'shop' => $snapshot['shop'],
|
||||
'growth' => $snapshot['growth'],
|
||||
'is_live' => $snapshot['is_live'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payline_points: float, paylines: int, pp: float, shop: float, growth: float, is_live: bool, failed: bool}|null
|
||||
*/
|
||||
private function calculateLive(User $user, int $month, int $year): ?array
|
||||
{
|
||||
try {
|
||||
$treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member');
|
||||
$treeCalcBot->initBusinesslUserDetail($user);
|
||||
$businessUser = $treeCalcBot->business_user;
|
||||
|
||||
if (! $businessUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Kein `?? 0` auf den Magic-Properties: die Items haben kein __isset,
|
||||
// isset() wäre dort immer false und das Ergebnis damit immer 0.
|
||||
return $this->buildSnapshot(
|
||||
(float) $businessUser->payline_points,
|
||||
$this->paylinesFromQualLevel($businessUser->qual_user_level),
|
||||
(float) $businessUser->commission_pp_total,
|
||||
(float) $businessUser->commission_shop_sales,
|
||||
(float) $businessUser->commission_growth_total,
|
||||
true
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning("LiveCommissionService: Live-Berechnung fehlgeschlagen für User {$user->id}: ".$e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Payline-Tiefe (Anzahl bezahlter Ebenen) aus dem Qualifikationslevel.
|
||||
*
|
||||
* @param array<string, mixed>|object|null $qualUserLevel
|
||||
*/
|
||||
private function paylinesFromQualLevel(mixed $qualUserLevel): int
|
||||
{
|
||||
if (is_array($qualUserLevel)) {
|
||||
return (int) ($qualUserLevel['paylines'] ?? 0);
|
||||
}
|
||||
|
||||
if (is_object($qualUserLevel)) {
|
||||
return (int) ($qualUserLevel->paylines ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payline_points: float, paylines: int, pp: float, shop: float, growth: float, is_live: bool, failed: bool}
|
||||
*/
|
||||
private function buildSnapshot(float $paylinePoints, int $paylines, float $pp, float $shop, float $growth, bool $isLive, bool $failed = false): array
|
||||
{
|
||||
return [
|
||||
'payline_points' => round($paylinePoints, 2),
|
||||
'paylines' => $paylines,
|
||||
'pp' => round($pp, 2),
|
||||
'shop' => round($shop, 2),
|
||||
'growth' => round($growth, 2),
|
||||
'is_live' => $isLive,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\TranslationHelper;
|
||||
use App\User;
|
||||
|
||||
/**
|
||||
* Klasse für die HTML-Darstellung von Business-Trees
|
||||
|
|
@ -11,8 +13,16 @@ use App\Services\TranslationHelper;
|
|||
class TreeHtmlRenderer
|
||||
{
|
||||
private string $initFrom;
|
||||
|
||||
private bool $forceLiveCalculation;
|
||||
|
||||
/**
|
||||
* Abo-Kennzeichen je User-ID (gebatcht geladen, nicht pro Knoten).
|
||||
*
|
||||
* @var array<int, array{own: bool, customer_count: int}>
|
||||
*/
|
||||
private array $aboBadges = [];
|
||||
|
||||
public function __construct(string $initFrom = 'member', bool $forceLiveCalculation = false)
|
||||
{
|
||||
$this->initFrom = $initFrom;
|
||||
|
|
@ -28,6 +38,8 @@ class TreeHtmlRenderer
|
|||
return '<div class="alert alert-info">Keine Business-User gefunden.</div>';
|
||||
}
|
||||
|
||||
$this->loadAboBadges($this->collectUserIds($businessUsers));
|
||||
|
||||
$html = '<ol class="dd-list">';
|
||||
foreach ($businessUsers as $businessUser) {
|
||||
$html .= $this->renderUserItem($businessUser, 0);
|
||||
|
|
@ -46,6 +58,8 @@ class TreeHtmlRenderer
|
|||
return '<div class="alert alert-info">Keine parentlosen User gefunden.</div>';
|
||||
}
|
||||
|
||||
$this->loadAboBadges($this->collectUserIds($parentless));
|
||||
|
||||
$html = '';
|
||||
foreach ($parentless as $item) {
|
||||
$html .= $this->renderParentlessItem($item);
|
||||
|
|
@ -59,14 +73,14 @@ class TreeHtmlRenderer
|
|||
*/
|
||||
public function renderSponsor($sponsor): string
|
||||
{
|
||||
if (!$sponsor) {
|
||||
return '<div class="alert alert-warning">' . __('team.no_sponsor_assigned') . '</div>';
|
||||
if (! $sponsor) {
|
||||
return '<div class="alert alert-warning">'.__('team.no_sponsor_assigned').'</div>';
|
||||
}
|
||||
|
||||
return '<li class="dd-item dd-nodrag" data-id="">' .
|
||||
'<div class="dd-handle">' .
|
||||
$this->renderUserInfo($sponsor, false, true) .
|
||||
'</div>' .
|
||||
return '<li class="dd-item dd-nodrag" data-id="">'.
|
||||
'<div class="dd-handle">'.
|
||||
$this->renderUserInfo($sponsor, false, true).
|
||||
'</div>'.
|
||||
'</li>';
|
||||
}
|
||||
|
||||
|
|
@ -91,9 +105,9 @@ class TreeHtmlRenderer
|
|||
/**
|
||||
* Rendert User Sponsor (für UserTeamCalcBot)
|
||||
*/
|
||||
public function renderUserSponsor(\App\User $sponsor): string
|
||||
public function renderUserSponsor(User $sponsor): string
|
||||
{
|
||||
if (!$sponsor || !$sponsor->account) {
|
||||
if (! $sponsor || ! $sponsor->account) {
|
||||
return '<div class="alert alert-info">Kein Sponsor gefunden.</div>';
|
||||
}
|
||||
|
||||
|
|
@ -103,19 +117,19 @@ class TreeHtmlRenderer
|
|||
|
||||
// Sponsor Info
|
||||
$html .= '<div class="col-md-3">';
|
||||
$html .= '<strong>' . e($sponsor->account->first_name . ' ' . $sponsor->account->last_name) . '</strong><br>';
|
||||
$html .= '<small>' . e($sponsor->email) . '</small>';
|
||||
$html .= '<strong>'.e($sponsor->account->first_name.' '.$sponsor->account->last_name).'</strong><br>';
|
||||
$html .= '<small>'.e($sponsor->email).'</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Account Info
|
||||
$html .= '<div class="col-md-2">';
|
||||
$html .= '<span class="badge badge-secondary">' . e($sponsor->account->m_account ?? '') . '</span>';
|
||||
$html .= '<span class="badge badge-secondary">'.e($sponsor->account->m_account ?? '').'</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Level Info
|
||||
$html .= '<div class="col-md-2">';
|
||||
if ($sponsor->user_level) {
|
||||
$html .= '<span class="badge badge-primary">' . e($sponsor->user_level->getLang('name')) . '</span>';
|
||||
$html .= '<span class="badge badge-primary">'.e($sponsor->user_level->getLang('name')).'</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
|
|
@ -136,7 +150,7 @@ class TreeHtmlRenderer
|
|||
*/
|
||||
private function renderTeamMemberItem($member, int $depth): string
|
||||
{
|
||||
$html = '<li class="dd-item" data-id="' . ($member->user_id ?? 0) . '">';
|
||||
$html = '<li class="dd-item" data-id="'.($member->user_id ?? 0).'">';
|
||||
$html .= '<div class="dd-handle">';
|
||||
$html .= '<div class="row">';
|
||||
|
||||
|
|
@ -146,19 +160,19 @@ class TreeHtmlRenderer
|
|||
// Name und Email
|
||||
$html .= '<div class="col-md-3">';
|
||||
$html .= $indent;
|
||||
$html .= '<strong>' . e(($member->first_name ?? '') . ' ' . ($member->last_name ?? '')) . '</strong><br>';
|
||||
$html .= $indent . '<small>' . e($member->email ?? '') . '</small>';
|
||||
$html .= '<strong>'.e(($member->first_name ?? '').' '.($member->last_name ?? '')).'</strong><br>';
|
||||
$html .= $indent.'<small>'.e($member->email ?? '').'</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Account ID
|
||||
$html .= '<div class="col-md-2">';
|
||||
$html .= '<span class="badge badge-secondary">' . e($member->m_account ?? '') . '</span>';
|
||||
$html .= '<span class="badge badge-secondary">'.e($member->m_account ?? '').'</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Level
|
||||
$html .= '<div class="col-md-2">';
|
||||
if (!empty($member->user_level_name)) {
|
||||
$html .= '<span class="badge badge-primary">' . e($member->user_level_name) . '</span>';
|
||||
if (! empty($member->user_level_name)) {
|
||||
$html .= '<span class="badge badge-primary">'.e($member->user_level_name).'</span>';
|
||||
|
||||
if ($member->next_qual_user_level) {
|
||||
$html .= '<span class="badge badge-outline-success ml-2"><i class="fa fa-arrow-up text-success" title="Karriere-Level erreicht!"></i></span>';
|
||||
|
|
@ -168,12 +182,12 @@ class TreeHtmlRenderer
|
|||
|
||||
// Qualifikation
|
||||
$html .= '<div class="col-md-2">';
|
||||
if (!empty($member->qual_kp)) {
|
||||
if (! empty($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>';
|
||||
$html .= '<span class="badge '.$badgeClass.'">KU '.$qualKP.'</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
|
|
@ -186,7 +200,7 @@ class TreeHtmlRenderer
|
|||
$html .= '</div>';
|
||||
|
||||
// Kinder rendern
|
||||
if (!empty($member->businessUserItems) && is_array($member->businessUserItems)) {
|
||||
if (! empty($member->businessUserItems) && is_array($member->businessUserItems)) {
|
||||
$html .= '<ol class="dd-list">';
|
||||
foreach ($member->businessUserItems as $child) {
|
||||
$html .= $this->renderTeamMemberItem($child, $depth + 1);
|
||||
|
|
@ -213,11 +227,11 @@ class TreeHtmlRenderer
|
|||
$childrenHtml .= '</ol>';
|
||||
}
|
||||
|
||||
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
|
||||
'<div class="dd-handle">' .
|
||||
$this->renderUserCardWithDepth($item, $deep) .
|
||||
'</div>' .
|
||||
$childrenHtml .
|
||||
return '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
|
||||
'<div class="dd-handle">'.
|
||||
$this->renderUserCardWithDepth($item, $deep).
|
||||
'</div>'.
|
||||
$childrenHtml.
|
||||
'</li>';
|
||||
}
|
||||
|
||||
|
|
@ -226,10 +240,10 @@ class TreeHtmlRenderer
|
|||
*/
|
||||
private function renderParentlessItem($item): string
|
||||
{
|
||||
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
|
||||
'<div class="dd-handle">' .
|
||||
$this->renderUserInfo($item, true, false) .
|
||||
'</div>' .
|
||||
return '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
|
||||
'<div class="dd-handle">'.
|
||||
$this->renderUserInfo($item, true, false).
|
||||
'</div>'.
|
||||
'</li>';
|
||||
}
|
||||
|
||||
|
|
@ -240,16 +254,16 @@ class TreeHtmlRenderer
|
|||
{
|
||||
$depthBadge = '';
|
||||
if ($deep > 0) {
|
||||
$depthBadge = '<div class="d-flex flex-column justify-content-center align-items-center">' .
|
||||
'<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">' . $deep . '</div>' .
|
||||
$depthBadge = '<div class="d-flex flex-column justify-content-center align-items-center">'.
|
||||
'<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">'.$deep.'</div>'.
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return '<div class="media align-items-center">' .
|
||||
$depthBadge .
|
||||
'<div class="media-body ml-2">' .
|
||||
$this->renderUserInfo($item, false, false) .
|
||||
'</div>' .
|
||||
return '<div class="media align-items-center">'.
|
||||
$depthBadge.
|
||||
'<div class="media-body ml-2">'.
|
||||
$this->renderUserInfo($item, false, false).
|
||||
'</div>'.
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
|
@ -263,35 +277,35 @@ class TreeHtmlRenderer
|
|||
|
||||
\Log::debug("TreeHtmlRenderer: Rendering user info for user {$item->user_id}");
|
||||
|
||||
$html = '<span class="' . $statusClass . '">';
|
||||
$html = '<span class="'.$statusClass.'">';
|
||||
|
||||
// User Link
|
||||
$html .= '<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" ' .
|
||||
'data-id="' . $item->user_id . '" data-action="business-user-show" data-back="" ' .
|
||||
'data-modal="modal-md" data-init_from="' . $this->initFrom . '" data-route="' . route('modal_load') . '">' .
|
||||
'<span class="mr-1 ion ion-ios-contact ' . $iconClass . '"></span> ' .
|
||||
'<strong>' . e($item->first_name . ' ' . $item->last_name) . '</strong>' .
|
||||
$html .= '<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" '.
|
||||
'data-id="'.$item->user_id.'" data-action="business-user-show" data-back="" '.
|
||||
'data-modal="modal-md" data-init_from="'.$this->initFrom.'" data-route="'.route('modal_load').'">'.
|
||||
'<span class="mr-1 ion ion-ios-contact '.$iconClass.'"></span> '.
|
||||
'<strong>'.e($item->first_name.' '.$item->last_name).'</strong>'.
|
||||
'</a>';
|
||||
|
||||
// Email
|
||||
$html .= ' <a href="mailto:' . e($item->email) . '">' . e($item->email) . '</a>';
|
||||
$html .= ' <a href="mailto:'.e($item->email).'">'.e($item->email).'</a>';
|
||||
|
||||
// Optional: Geburtstag
|
||||
$birthday = $item->user_birthday; // Magic Method __get() verwenden
|
||||
if ($birthday && trim($birthday) !== '') {
|
||||
$html .= ' | <i class="ion ion-ios-gift text-primary"></i> ' . e($birthday);
|
||||
$html .= ' | <i class="ion ion-ios-gift text-primary"></i> '.e($birthday);
|
||||
}
|
||||
|
||||
// Optional: Telefon
|
||||
$phone = $item->user_phone; // Magic Method __get() verwenden
|
||||
if ($phone && trim($phone) !== '') {
|
||||
$html .= ' | <i class="ion ion-ios-call text-primary"></i> ' . e($phone);
|
||||
$html .= ' | <i class="ion ion-ios-call text-primary"></i> '.e($phone);
|
||||
}
|
||||
|
||||
// Level Badge
|
||||
$levelName = $item->user_level_name ? TranslationHelper::transUserLevelName($item->user_level_name) : '';
|
||||
$account = $item->m_account ?: '';
|
||||
$html .= ' <span class="badge badge-outline-default ' . $statusClass . '">' . e($levelName . ' | ' . $account) . '</span>';
|
||||
$html .= ' <span class="badge badge-outline-default '.$statusClass.'">'.e($levelName.' | '.$account).'</span>';
|
||||
|
||||
// Karriere-Aufstiegs-Icon für qualifizierte User (nur in Struktur-Ansicht)#
|
||||
|
||||
|
|
@ -299,15 +313,18 @@ class TreeHtmlRenderer
|
|||
$html .= '<span class="badge badge-outline-success ml-2"><i class="fa fa-arrow-up text-success" title="Karriere-Level erreicht!"></i></span>';
|
||||
}
|
||||
|
||||
// Abo-Kennzeichen (eigenes Abo + eigene Kundenabos)
|
||||
$html .= $this->renderAboBadges((int) $item->user_id);
|
||||
|
||||
// Details für aktive Accounts
|
||||
if ($item->active_account) {
|
||||
$html .= '<br><span class="small">';
|
||||
if (!$isSponsor) {
|
||||
if (! $isSponsor) {
|
||||
$html .= $this->renderAccountDetails($item);
|
||||
}
|
||||
|
||||
// Action Button (außer für Sponsor-Ansicht)
|
||||
if (!$isSponsor && $this->shouldShowActionButton()) {
|
||||
if (! $isSponsor && $this->shouldShowActionButton()) {
|
||||
$html .= $this->renderActionButton($item->user_id);
|
||||
}
|
||||
|
||||
|
|
@ -315,12 +332,12 @@ class TreeHtmlRenderer
|
|||
} else {
|
||||
// Inaktive Accounts
|
||||
$paymentDate = $item->payment_account_date ?: '';
|
||||
$html .= '<br><span class="small">' . __('team.account_to') . ': ' . e($paymentDate) . '</span>';
|
||||
$html .= '<br><span class="small">'.__('team.account_to').': '.e($paymentDate).'</span>';
|
||||
}
|
||||
|
||||
// Sponsor für parentlose User
|
||||
if ($showSponsor && $item->m_sponsor_name) {
|
||||
$html .= '<br>' . e($item->m_sponsor_name);
|
||||
$html .= '<br>'.e($item->m_sponsor_name);
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
|
@ -341,12 +358,12 @@ class TreeHtmlRenderer
|
|||
$eSum = $item->sales_volume_total ?: 0;
|
||||
$sSum = $item->sales_volume_total_shop ?: 0;
|
||||
|
||||
return '<strong>' . __('team.total_points') . ': ' . formatNumber($totalPoints) . '</strong> | ' .
|
||||
__('team.e') . ': ' . formatNumber($ePoints) . ' | ' .
|
||||
__('team.s') . ': ' . formatNumber($sPoints) . ' <strong> | ' .
|
||||
__('team.net_turnover') . ': ' . formatNumber($totalSum) . ' €</strong> | ' .
|
||||
__('team.e') . ': ' . formatNumber($eSum) . ' € | ' .
|
||||
__('team.s') . ': ' . formatNumber($sSum) . ' €';
|
||||
return '<strong>'.__('team.total_points').': '.formatNumber($totalPoints).'</strong> | '.
|
||||
__('team.e').': '.formatNumber($ePoints).' | '.
|
||||
__('team.s').': '.formatNumber($sPoints).' <strong> | '.
|
||||
__('team.net_turnover').': '.formatNumber($totalSum).' €</strong> | '.
|
||||
__('team.e').': '.formatNumber($eSum).' € | '.
|
||||
__('team.s').': '.formatNumber($sSum).' €';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -354,15 +371,15 @@ class TreeHtmlRenderer
|
|||
*/
|
||||
private function renderActionButton(int $userId): string
|
||||
{
|
||||
return ' | <button type="button" class="btn icon-btn btn-xs btn-secondary" ' .
|
||||
'data-toggle="modal" data-target="#modals-load-content" ' .
|
||||
'data-id="' . $userId . '" data-action="business-user-detail" ' .
|
||||
'data-back="" data-modal="modal-xl" ' .
|
||||
'data-init_from="' . $this->initFrom . '" ' .
|
||||
'data-live="' . $this->forceLiveCalculation . '" ' .
|
||||
'data-optimized="1" ' .
|
||||
'data-route="' . route('modal_load') . '">' .
|
||||
'<span class="fa fa-calculator"></span>' .
|
||||
return ' | <button type="button" class="btn icon-btn btn-xs btn-secondary" '.
|
||||
'data-toggle="modal" data-target="#modals-load-content" '.
|
||||
'data-id="'.$userId.'" data-action="business-user-detail" '.
|
||||
'data-back="" data-modal="modal-xl" '.
|
||||
'data-init_from="'.$this->initFrom.'" '.
|
||||
'data-live="'.$this->forceLiveCalculation.'" '.
|
||||
'data-optimized="1" '.
|
||||
'data-route="'.route('modal_load').'">'.
|
||||
'<span class="fa fa-calculator"></span>'.
|
||||
'</button>';
|
||||
}
|
||||
|
||||
|
|
@ -380,12 +397,100 @@ class TreeHtmlRenderer
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Abo-Kennzeichen für alle übergebenen User-IDs gebatcht
|
||||
* (zwei Group-by-Queries statt Einzelabfragen pro Knoten).
|
||||
*
|
||||
* Zählweise konsistent zur Backoffice-Statistik: aktive Abos = active=true
|
||||
* und status NOT IN (4,5,6) via {@see UserAbo::scopeActiveAbo()}.
|
||||
*
|
||||
* @param array<int> $userIds
|
||||
*/
|
||||
public function loadAboBadges(array $userIds): void
|
||||
{
|
||||
$userIds = array_values(array_unique(array_filter($userIds)));
|
||||
|
||||
if (empty($userIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ownAboUserIds = UserAbo::query()
|
||||
->activeAbo()
|
||||
->where('is_for', 'me')
|
||||
->whereIn('user_id', $userIds)
|
||||
->distinct()
|
||||
->pluck('user_id');
|
||||
|
||||
foreach ($ownAboUserIds as $userId) {
|
||||
$this->aboBadges[(int) $userId]['own'] = true;
|
||||
}
|
||||
|
||||
$customerAboCounts = UserAbo::query()
|
||||
->activeAbo()
|
||||
->where('is_for', 'ot')
|
||||
->whereIn('member_id', $userIds)
|
||||
->groupBy('member_id')
|
||||
->selectRaw('member_id, COUNT(*) as abo_count')
|
||||
->pluck('abo_count', 'member_id');
|
||||
|
||||
foreach ($customerAboCounts as $userId => $count) {
|
||||
$this->aboBadges[(int) $userId]['customer_count'] = (int) $count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Abo-Kennzeichen für einen User:
|
||||
* grünes "A"-Badge bei eigenem aktiven Abo, rotes "(n)"-Badge bei n > 0 eigenen aktiven Kundenabos.
|
||||
*/
|
||||
public function renderAboBadges(int $userId): string
|
||||
{
|
||||
$hasOwnAbo = $this->aboBadges[$userId]['own'] ?? false;
|
||||
$customerAboCount = $this->aboBadges[$userId]['customer_count'] ?? 0;
|
||||
|
||||
$html = '';
|
||||
|
||||
if ($hasOwnAbo) {
|
||||
$html .= ' <span class="badge badge-success" title="'.e(__('team.badge_own_abo')).'">A</span>';
|
||||
}
|
||||
|
||||
if ($customerAboCount > 0) {
|
||||
$html .= ' <span class="badge badge-danger" title="'.
|
||||
e(trans_choice('team.badge_customer_abos', $customerAboCount)).'">('.$customerAboCount.')</span>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt rekursiv alle User-IDs aus den Business-User-Items.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
private function collectUserIds(array $items): array
|
||||
{
|
||||
$userIds = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$userId = (int) $item->user_id;
|
||||
if ($userId > 0) {
|
||||
$userIds[] = $userId;
|
||||
}
|
||||
|
||||
if (isset($item->businessUserItems) && is_array($item->businessUserItems) && count($item->businessUserItems) > 0) {
|
||||
$userIds = array_merge($userIds, $this->collectUserIds($item->businessUserItems));
|
||||
}
|
||||
}
|
||||
|
||||
return $userIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Kontext für die Darstellung
|
||||
*/
|
||||
public function setInitFrom(string $initFrom): self
|
||||
{
|
||||
$this->initFrom = $initFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ class Credit
|
|||
return (int) Setting::getContentBySlug('invoice-number');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vergibt atomar die nächste Gutschriftsnummer aus dem gemeinsamen
|
||||
* Nummernkreis (Setting 'invoice-number') — gleicher Lock-Mechanismus wie
|
||||
* bei Rechnungen, damit parallele Läufe keine doppelten Nummern erzeugen.
|
||||
*/
|
||||
public static function makeNextCreditNumber()
|
||||
{
|
||||
$credit_number = self::getCreditNumber();
|
||||
$credit_number = $credit_number + 1;
|
||||
// Setting::setContentBySlug('credit-number', $credit_number, 'int');
|
||||
Setting::setContentBySlug('invoice-number', $credit_number, 'int');
|
||||
|
||||
return $credit_number;
|
||||
return Invoice::makeNextInvoiceNumber();
|
||||
}
|
||||
|
||||
public static function createCreditNumber($credit_number, $credit_date)
|
||||
|
|
|
|||
|
|
@ -282,13 +282,13 @@ class DatevExportService
|
|||
$isEu = $this->isEuCountry($country);
|
||||
|
||||
$euUstid = $this->resolveEuUstid($order, $isDomestic);
|
||||
$hasValidUstid = ! empty($euUstid);
|
||||
|
||||
if (! $isDomestic && $country) {
|
||||
$euLand = $country->code ?? null;
|
||||
}
|
||||
|
||||
// Tax-Split vorhanden? -> Mehrere Zeilen pro Steuersatz
|
||||
// Tax-Split vorhanden? -> Bestellung wurde MIT USt berechnet:
|
||||
// Mehrere Zeilen pro Steuersatz, nie steuerfreie igL.
|
||||
if ($order->tax_split && is_array($order->tax_split) && count($order->tax_split) > 0) {
|
||||
$netSplit = $this->resolveNetSplit($order);
|
||||
|
||||
|
|
@ -307,7 +307,7 @@ class DatevExportService
|
|||
continue;
|
||||
}
|
||||
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $hasValidUstid);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, false);
|
||||
|
||||
$lines->push([
|
||||
'source_type' => DatevExportLine::SOURCE_INVOICE,
|
||||
|
|
@ -327,15 +327,18 @@ class DatevExportService
|
|||
]);
|
||||
}
|
||||
} else {
|
||||
// Kein Tax-Split: Gesamtbetrag als eine Zeile
|
||||
// Kein Tax-Split -> Bestellung wurde OHNE USt verkauft (igL/Drittland)
|
||||
// oder Inland-Altfall. EU-Lieferungen ohne USt sind steuerfreie igL,
|
||||
// unabhängig davon, ob die USt-ID heute noch am Account hinterlegt ist.
|
||||
$grossAmount = floatval($order->total_shipping ?? $order->total ?? 0);
|
||||
|
||||
if ($grossAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$soldTaxFree = $isEu && ! $isDomestic;
|
||||
$taxRate = intval($order->tax_rate ?? 19);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $hasValidUstid);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $soldTaxFree);
|
||||
|
||||
$lines->push([
|
||||
'source_type' => DatevExportLine::SOURCE_INVOICE,
|
||||
|
|
@ -390,8 +393,8 @@ class DatevExportService
|
|||
$user = $credit->user;
|
||||
$account = $user ? $user->account : null;
|
||||
|
||||
// Steuerstatus des Beraters ermitteln
|
||||
$taxStatus = $this->determineCommissionTaxStatus($account);
|
||||
// Steuerstatus aus dem Snapshot der Gutschrift (nicht aktueller Account-Stand)
|
||||
$taxStatus = $this->determineCommissionTaxStatus($account, $credit);
|
||||
$buSchluessel = config('datev.commission_tax_keys.'.$taxStatus, 9);
|
||||
|
||||
// USt-ID für Reverse Charge
|
||||
|
|
@ -424,10 +427,10 @@ class DatevExportService
|
|||
$konto = $commissionAccounts[$type] ?? $commissionAccounts['shop'];
|
||||
|
||||
// Bei USt-pflichtigen Beratern: Brutto berechnen
|
||||
// (Steuersatz aus dem Snapshot der Gutschrift, wie auf dem Beleg ausgewiesen)
|
||||
$grossAmount = $totalAmount;
|
||||
if ($taxStatus === 'normal' && $credit->taxable == 1) {
|
||||
// Netto + USt = Brutto
|
||||
$taxRate = config('app.main_tax_rate', 19);
|
||||
if ($taxStatus === 'normal') {
|
||||
$taxRate = floatval($credit->tax_rate) ?: config('app.main_tax_rate', 19);
|
||||
$grossAmount = round($totalAmount * (1 + $taxRate / 100), 2);
|
||||
}
|
||||
|
||||
|
|
@ -513,7 +516,6 @@ class DatevExportService
|
|||
$isEu = $this->isEuCountry($country);
|
||||
$euLand = (! $isDomestic && $country) ? ($country->code ?? null) : null;
|
||||
$euUstid = $this->resolveEuUstid($order, $isDomestic);
|
||||
$hasValidUstid = ! empty($euUstid);
|
||||
|
||||
if ($order->tax_split && is_array($order->tax_split) && count($order->tax_split) > 0) {
|
||||
$netSplit = $this->resolveNetSplit($order);
|
||||
|
|
@ -532,7 +534,7 @@ class DatevExportService
|
|||
continue;
|
||||
}
|
||||
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $hasValidUstid);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, false);
|
||||
|
||||
// Storno: Negativer Betrag
|
||||
$lines->push([
|
||||
|
|
@ -558,8 +560,9 @@ class DatevExportService
|
|||
continue;
|
||||
}
|
||||
|
||||
$soldTaxFree = $isEu && ! $isDomestic;
|
||||
$taxRate = intval($order->tax_rate ?? 19);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $hasValidUstid);
|
||||
$revenueMapping = $this->determineRevenueAccount($taxRate, $isDomestic, $isEu, $soldTaxFree);
|
||||
|
||||
$lines->push([
|
||||
'source_type' => DatevExportLine::SOURCE_CANCELLATION,
|
||||
|
|
@ -831,12 +834,15 @@ class DatevExportService
|
|||
*/
|
||||
|
||||
/**
|
||||
* Bestimmt das Erlöskonto und den BU-Schlüssel basierend auf Steuersatz, Land und USt-ID.
|
||||
* Bestimmt das Erlöskonto und den BU-Schlüssel basierend auf Steuersatz, Land
|
||||
* und der tatsächlichen Besteuerung der Bestellung.
|
||||
*
|
||||
* EU-steuerfreie IG-Lieferung (BU 1 / Konto 8125) nur wenn eine gültige USt-ID vorliegt.
|
||||
* Ohne USt-ID wird auch bei EU-Lieferungen die reguläre Inlandsbesteuerung angewendet.
|
||||
* Maßgeblich ist, wie die Rechnung ausgestellt wurde ($soldTaxFree = ohne USt
|
||||
* verkauft), nicht der aktuelle Account-Zustand: EU-Bestellungen ohne USt sind
|
||||
* steuerfreie IG-Lieferungen (BU 1 / Konto 8125); mit USt berechnete
|
||||
* EU-Bestellungen werden wie Inland gebucht.
|
||||
*/
|
||||
private function determineRevenueAccount(int $taxRate, bool $isDomestic, bool $isEu, bool $hasValidUstid = false): array
|
||||
private function determineRevenueAccount(int $taxRate, bool $isDomestic, bool $isEu, bool $soldTaxFree = false): array
|
||||
{
|
||||
$accounts = config('datev.revenue_accounts');
|
||||
|
||||
|
|
@ -844,7 +850,7 @@ class DatevExportService
|
|||
return $accounts['third_country_exempt'];
|
||||
}
|
||||
|
||||
if ($isEu && ! $isDomestic && $hasValidUstid) {
|
||||
if ($isEu && ! $isDomestic && $soldTaxFree) {
|
||||
return $accounts['eu_exempt'];
|
||||
}
|
||||
|
||||
|
|
@ -884,33 +890,55 @@ class DatevExportService
|
|||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den Steuerstatus eines Beraters für die Provision.
|
||||
* Bestimmt den Steuerstatus einer Provisions-Gutschrift.
|
||||
*
|
||||
* Maßgeblich ist der taxable-Snapshot der Gutschrift (Zustand zum
|
||||
* Ausstellungszeitpunkt, wie auf dem Beleg ausgewiesen), NICHT der aktuelle
|
||||
* Account-Zustand. Nur Alt-Gutschriften ohne Snapshot (taxable=0) fallen
|
||||
* auf die Account-Logik zurück.
|
||||
*/
|
||||
private function determineCommissionTaxStatus($account): string
|
||||
private function determineCommissionTaxStatus($account, $credit = null): string
|
||||
{
|
||||
$taxable = intval($credit->taxable ?? 0);
|
||||
|
||||
if ($taxable === 1) {
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
if ($taxable === 2) {
|
||||
return 'kleinunternehmer';
|
||||
}
|
||||
|
||||
if ($taxable === 3) {
|
||||
// Ausland: EU -> Reverse Charge, Drittland -> kein BU nötig, KU als Fallback
|
||||
$country = $account?->country;
|
||||
if ($country && $country->eu_country) {
|
||||
return 'reverse_charge';
|
||||
}
|
||||
|
||||
return 'kleinunternehmer';
|
||||
}
|
||||
|
||||
// Fallback für Alt-Gutschriften ohne taxable-Snapshot
|
||||
if (! $account) {
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Reverse Charge hat Vorrang
|
||||
if ($account->reverse_charge) {
|
||||
return 'reverse_charge';
|
||||
}
|
||||
|
||||
// Kleinunternehmer (taxable_sales = 2)
|
||||
if ($account->taxable_sales == 2) {
|
||||
return 'kleinunternehmer';
|
||||
}
|
||||
|
||||
// Ausland nicht USt-pflichtig (taxable_sales = 3)
|
||||
// Wird wie Reverse Charge behandelt, falls aus EU
|
||||
if ($account->taxable_sales == 3) {
|
||||
$country = $account->country;
|
||||
if ($country && $country->eu_country) {
|
||||
return 'reverse_charge';
|
||||
}
|
||||
|
||||
return 'kleinunternehmer'; // Drittland: kein BU nötig, aber KU als Fallback
|
||||
return 'kleinunternehmer';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
|
|
@ -970,8 +998,8 @@ class DatevExportService
|
|||
|
||||
/**
|
||||
* Ermittelt die USt-ID des Bestellers (auth_user).
|
||||
* Nur registrierte Berater mit hinterlegter USt-ID qualifizieren sich
|
||||
* für steuerfreie innergemeinschaftliche Lieferungen.
|
||||
* Deutsche oder formal unplausible USt-IDs werden verworfen — sie
|
||||
* qualifizieren nie für steuerfreie innergemeinschaftliche Lieferungen.
|
||||
*/
|
||||
private function resolveEuUstid($order, bool $isDomestic): ?string
|
||||
{
|
||||
|
|
@ -979,7 +1007,13 @@ class DatevExportService
|
|||
return null;
|
||||
}
|
||||
|
||||
return $order->auth_user?->account?->tax_identification_number ?: null;
|
||||
$ustid = $order->auth_user?->account?->tax_identification_number ?: null;
|
||||
|
||||
if ($ustid && ! app(VatIdService::class)->qualifiesForReverseCharge($ustid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ustid;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1064,10 +1098,11 @@ class DatevExportService
|
|||
}
|
||||
|
||||
/**
|
||||
* Parst einen formatierten Zahlenwert (z.B. "5.00" oder "5,00") zu float.
|
||||
* Parst einen formatierten Zahlenwert (z.B. "5.00", "5,00" oder "1,434.70") zu float.
|
||||
* Behandelt sowohl einfache Werte als auch tax_split Arrays (homeparty).
|
||||
* Tausendertrennzeichen ("1,434.70" oder "1.434,70") werden korrekt entfernt.
|
||||
*/
|
||||
private function parseNumber($value, ?string $preferredArrayKey = null): float
|
||||
public function parseNumber($value, ?string $preferredArrayKey = null): float
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$arrayKeys = $this->getSplitArrayKeys($preferredArrayKey);
|
||||
|
|
@ -1081,7 +1116,38 @@ class DatevExportService
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
return floatval(str_replace(',', '.', $value));
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$hasComma = str_contains($value, ',');
|
||||
$hasDot = str_contains($value, '.');
|
||||
|
||||
if ($hasComma && $hasDot) {
|
||||
if (strrpos($value, ',') > strrpos($value, '.')) {
|
||||
// Deutsches Format: 1.434,70
|
||||
$value = str_replace('.', '', $value);
|
||||
$value = str_replace(',', '.', $value);
|
||||
} else {
|
||||
// US-Format: 1,434.70
|
||||
$value = str_replace(',', '', $value);
|
||||
}
|
||||
} elseif ($hasComma) {
|
||||
if (preg_match('/^-?\d{1,3}(,\d{3})+$/', $value)) {
|
||||
// Nur Tausendertrenner: 1,434
|
||||
$value = str_replace(',', '', $value);
|
||||
} else {
|
||||
// Komma als Dezimaltrenner: 5,00
|
||||
$value = str_replace(',', '.', $value);
|
||||
}
|
||||
}
|
||||
|
||||
return floatval($value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
353
app/Services/GdprDeletionService.php
Normal file
353
app/Services/GdprDeletionService.php
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserShop;
|
||||
use App\Models\UserUpdateEmail;
|
||||
use App\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* DSGVO-Löschung (Art. 17 DSGVO) für Berater (User) und Endkunden (ShoppingUser).
|
||||
*
|
||||
* Personenbezogene Daten werden unwiderruflich anonymisiert. Daten, die unter
|
||||
* gesetzliche Aufbewahrungspflichten fallen (Rechnungen, Gutschriften,
|
||||
* DATEV-Exporte, Provisionsabrechnungen, Bestellungen mit Beleg), bleiben
|
||||
* gemäß Art. 17 Abs. 3 lit. b DSGVO erhalten:
|
||||
* - Rechnungs-/Gutschrift-PDFs und deren DB-Zeilen werden nicht verändert.
|
||||
* - Bei Kunden-Datensätzen (shopping_users) mit Bestellungen bleiben Name und
|
||||
* Rechnungsadresse erhalten (Buchungsbeleg), Kontaktdaten werden entfernt.
|
||||
* - Provisions-Snapshots (user_businesses) behalten die Zahlen, die
|
||||
* denormalisierten Personendaten werden anonymisiert.
|
||||
*/
|
||||
class GdprDeletionService
|
||||
{
|
||||
public const ANONYMIZED_FIRSTNAME = 'Gelöscht';
|
||||
|
||||
public const ANONYMIZED_LASTNAME = 'DSGVO';
|
||||
|
||||
/**
|
||||
* Berater (User) DSGVO-konform löschen.
|
||||
*
|
||||
* @return array<string, int|string> Zusammenfassung der ausgeführten Schritte
|
||||
*
|
||||
* @throws GdprDeletionException wenn Sperrgründe vorliegen (Admin-Account, aktives Abo)
|
||||
*/
|
||||
public function deleteUser(User $user, ?int $executedByUserId = null): array
|
||||
{
|
||||
if ($user->admin >= 2) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_is_admin'));
|
||||
}
|
||||
|
||||
$activeAbos = UserAbo::where('user_id', $user->id)->where('active', true)->count();
|
||||
if ($activeAbos > 0) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_active_abo'));
|
||||
}
|
||||
|
||||
$originalEmail = $user->email;
|
||||
|
||||
$summary = DB::transaction(function () use ($user) {
|
||||
$summary = ['user_id' => $user->id];
|
||||
|
||||
// MLM-Struktur erhalten: Downline und Kunden auf den nächsten aktiven Sponsor
|
||||
// übertragen. Bewusst beim Sponsor starten, nicht beim User selbst — ein noch
|
||||
// aktiver User würde sonst sich selbst als neuen Sponsor erhalten.
|
||||
$active_sponsor = UserUtil::findNextActiveSponsor($user->m_sponsor ?: $user->pre_sponsor);
|
||||
if ($active_sponsor && $active_sponsor->id !== $user->id) {
|
||||
UserUtil::setNewSponsorToChilds($user->id, $active_sponsor->id);
|
||||
UserUtil::setShoppingUserToNewMember($user->id, $active_sponsor->id);
|
||||
$summary['new_sponsor_id'] = $active_sponsor->id;
|
||||
}
|
||||
|
||||
// Bestehende Komplett-Löschung: E-Mail randomisieren, Zugangsdaten entwerten,
|
||||
// Account + Shop soft-deleten
|
||||
UserUtil::deleteUser($user, true);
|
||||
|
||||
$this->anonymizeUserRow($user);
|
||||
$this->anonymizeUserAccount($user);
|
||||
$this->anonymizeUserShop($user);
|
||||
$summary['files_deleted'] = $this->deleteUserFiles($user);
|
||||
$summary['update_emails_deleted'] = UserUpdateEmail::where('user_id', $user->id)->delete();
|
||||
$summary['abos_anonymized'] = $this->anonymizeUserAbos('user_id', $user->id);
|
||||
$summary['business_rows_anonymized'] = $this->anonymizeUserBusinessRows($user);
|
||||
$summary['shopping_users_anonymized'] = $this->anonymizeShoppingUserRows(
|
||||
ShoppingUser::withTrashed()->where('auth_user_id', $user->id)->get()
|
||||
);
|
||||
|
||||
return $summary;
|
||||
});
|
||||
|
||||
$this->logDeletion('gdpr-delete-user', $user->id, User::class, $executedByUserId, $originalEmail, $summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Endkunden (ShoppingUser) DSGVO-konform löschen.
|
||||
*
|
||||
* Anonymisiert alle Datensätze des Kunden (Einträge mit gleicher Kundennummer).
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*
|
||||
* @throws GdprDeletionException wenn der Datensatz zu einem Berater gehört oder ein aktives Abo besteht
|
||||
*/
|
||||
public function deleteShoppingUser(ShoppingUser $shoppingUser, ?int $executedByUserId = null): array
|
||||
{
|
||||
if ($shoppingUser->auth_user_id) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_customer_is_member'));
|
||||
}
|
||||
|
||||
$rows = $this->getCustomerRows($shoppingUser);
|
||||
|
||||
$activeAbos = UserAbo::whereIn('shopping_user_id', $rows->pluck('id'))->where('active', true)->count();
|
||||
if ($activeAbos > 0) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_active_abo'));
|
||||
}
|
||||
|
||||
$originalEmail = $shoppingUser->billing_email;
|
||||
|
||||
$summary = DB::transaction(function () use ($shoppingUser, $rows) {
|
||||
$summary = ['shopping_user_id' => $shoppingUser->id];
|
||||
$summary['abos_anonymized'] = 0;
|
||||
foreach ($rows as $row) {
|
||||
$summary['abos_anonymized'] += $this->anonymizeUserAbos('shopping_user_id', $row->id);
|
||||
}
|
||||
$summary['shopping_users_anonymized'] = $this->anonymizeShoppingUserRows($rows);
|
||||
|
||||
return $summary;
|
||||
});
|
||||
|
||||
$this->logDeletion('gdpr-delete-customer', $shoppingUser->id, ShoppingUser::class, $executedByUserId, $originalEmail, $summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Datensätze eines Endkunden (gleiche Kundennummer), inkl. soft-deleted.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, ShoppingUser>
|
||||
*/
|
||||
public function getCustomerRows(ShoppingUser $shoppingUser)
|
||||
{
|
||||
if ($shoppingUser->number > 0) {
|
||||
return ShoppingUser::withTrashed()->where('number', $shoppingUser->number)->get();
|
||||
}
|
||||
|
||||
return ShoppingUser::withTrashed()->where('id', $shoppingUser->id)->get();
|
||||
}
|
||||
|
||||
protected function anonymizeUserRow(User $user): void
|
||||
{
|
||||
$user->notes = null;
|
||||
$user->settings = null;
|
||||
$user->payment_methods = null;
|
||||
$user->last_login = null;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
protected function anonymizeUserAccount(User $user): void
|
||||
{
|
||||
if (! $user->account_id) {
|
||||
return;
|
||||
}
|
||||
$account = UserAccount::withTrashed()->find($user->account_id);
|
||||
if (! $account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account->m_salutation = null;
|
||||
$account->m_first_name = self::ANONYMIZED_FIRSTNAME;
|
||||
$account->m_last_name = self::ANONYMIZED_LASTNAME;
|
||||
$account->m_notes = null;
|
||||
$account->company = null;
|
||||
$account->salutation = null;
|
||||
$account->first_name = self::ANONYMIZED_FIRSTNAME;
|
||||
$account->last_name = self::ANONYMIZED_LASTNAME;
|
||||
$account->address = null;
|
||||
$account->address_2 = null;
|
||||
$account->zipcode = null;
|
||||
$account->city = null;
|
||||
$account->pre_phone_id = null;
|
||||
$account->phone = null;
|
||||
$account->pre_mobil_id = null;
|
||||
$account->mobil = null;
|
||||
$account->tax_number = null;
|
||||
$account->tax_identification_number = null;
|
||||
$account->shipping_salutation = null;
|
||||
$account->shipping_company = null;
|
||||
$account->shipping_firstname = null;
|
||||
$account->shipping_lastname = null;
|
||||
$account->shipping_address = null;
|
||||
$account->shipping_address_2 = null;
|
||||
$account->shipping_zipcode = null;
|
||||
$account->shipping_city = null;
|
||||
$account->shipping_pre_phone_id = null;
|
||||
$account->shipping_phone = null;
|
||||
$account->shipping_postnumber = null;
|
||||
$account->birthday = null;
|
||||
$account->website = null;
|
||||
$account->facebook = null;
|
||||
$account->facebook_fanpage = null;
|
||||
$account->instagram = null;
|
||||
$account->bank_owner = null;
|
||||
$account->bank_iban = null;
|
||||
$account->bank_bic = null;
|
||||
$account->payment_data = null;
|
||||
$account->notice = null;
|
||||
$account->save();
|
||||
}
|
||||
|
||||
protected function anonymizeUserShop(User $user): void
|
||||
{
|
||||
$shop = UserShop::withTrashed()->where('user_id', $user->id)->first();
|
||||
if (! $shop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($shop->filename) {
|
||||
Storage::disk('public')->delete('images/shop/'.$shop->filename);
|
||||
}
|
||||
$shop->title = null;
|
||||
$shop->contact = null;
|
||||
$shop->accessibility = null;
|
||||
$shop->about = null;
|
||||
$shop->trans = null;
|
||||
$shop->filename = null;
|
||||
$shop->originalname = null;
|
||||
$shop->ext = null;
|
||||
$shop->mine = null;
|
||||
$shop->size = null;
|
||||
$shop->save();
|
||||
}
|
||||
|
||||
protected function deleteUserFiles(User $user): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($user->files()->get() as $file) {
|
||||
Storage::disk('user')->delete($file->dir.$file->filename);
|
||||
$file->delete();
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zahlungs-Tokens und Kontaktdaten aller Abos entfernen (auch soft-deleted).
|
||||
*/
|
||||
protected function anonymizeUserAbos(string $column, int $id): int
|
||||
{
|
||||
$count = 0;
|
||||
$abos = UserAbo::withTrashed()->where($column, $id)->get();
|
||||
foreach ($abos as $abo) {
|
||||
$abo->email = null;
|
||||
$abo->carddata = null;
|
||||
$abo->payone_userid = 0;
|
||||
$abo->stripe_customer_id = null;
|
||||
$abo->stripe_payment_method_id = null;
|
||||
$abo->active = false;
|
||||
$abo->save();
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalisierte Personendaten in den monatlichen Provisions-Snapshots
|
||||
* anonymisieren; die Finanzdaten (Umsätze, Provisionen) bleiben erhalten.
|
||||
*/
|
||||
protected function anonymizeUserBusinessRows(User $user): int
|
||||
{
|
||||
return UserBusiness::where('user_id', $user->id)->update([
|
||||
'email' => null,
|
||||
'first_name' => self::ANONYMIZED_FIRSTNAME,
|
||||
'last_name' => self::ANONYMIZED_LASTNAME,
|
||||
'user_birthday' => null,
|
||||
'user_phone' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kunden-Datensätze anonymisieren. Datensätze mit Bestellungen behalten
|
||||
* Name und Rechnungsadresse (Aufbewahrungspflicht Buchungsbeleg),
|
||||
* verlieren aber alle Kontaktdaten. Datensätze ohne Bestellung werden
|
||||
* vollständig anonymisiert.
|
||||
*
|
||||
* @param Collection<int, ShoppingUser> $rows
|
||||
*/
|
||||
protected function anonymizeShoppingUserRows($rows): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($rows as $row) {
|
||||
$hasOrders = $row->shopping_orders()->withTrashed()->count() > 0;
|
||||
if ($hasOrders) {
|
||||
$this->anonymizeShoppingUserContactData($row);
|
||||
} else {
|
||||
$this->anonymizeShoppingUserComplete($row);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
protected function anonymizeShoppingUserContactData(ShoppingUser $row): void
|
||||
{
|
||||
$row->billing_phone = null;
|
||||
$row->billing_email = 'geloescht_'.$row->id.'@dsgvo.invalid';
|
||||
$row->shipping_phone = null;
|
||||
$row->shipping_email = null;
|
||||
$row->remarks = '';
|
||||
$row->notice = [];
|
||||
$row->subscribed = false;
|
||||
$row->faker_mail = true;
|
||||
$row->save();
|
||||
}
|
||||
|
||||
protected function anonymizeShoppingUserComplete(ShoppingUser $row): void
|
||||
{
|
||||
$row->billing_salutation = null;
|
||||
$row->billing_company = null;
|
||||
$row->billing_firstname = self::ANONYMIZED_FIRSTNAME;
|
||||
$row->billing_lastname = self::ANONYMIZED_LASTNAME;
|
||||
$row->billing_address = null;
|
||||
$row->billing_address_2 = null;
|
||||
$row->billing_zipcode = null;
|
||||
$row->billing_city = null;
|
||||
$row->shipping_salutation = null;
|
||||
$row->shipping_company = null;
|
||||
$row->shipping_firstname = null;
|
||||
$row->shipping_lastname = null;
|
||||
$row->shipping_address = null;
|
||||
$row->shipping_address_2 = null;
|
||||
$row->shipping_zipcode = null;
|
||||
$row->shipping_city = null;
|
||||
$row->shipping_postnumber = null;
|
||||
$this->anonymizeShoppingUserContactData($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nachweis der Löschung (Art. 5 Abs. 2 DSGVO) ohne personenbezogene Daten.
|
||||
*
|
||||
* @param array<string, int|string> $summary
|
||||
*/
|
||||
protected function logDeletion(string $action, int $modelId, string $modelClass, ?int $executedByUserId, ?string $originalEmail, array $summary): void
|
||||
{
|
||||
SysLog::action($action, 'gdpr', 4)
|
||||
->setUserId($executedByUserId)
|
||||
->setModel($modelId, $modelClass)
|
||||
->setMessage('DSGVO-Löschung ausgeführt. Umfang: '.json_encode($summary))
|
||||
->save();
|
||||
|
||||
Log::channel('cleanup')->info($action.' id:'.$modelId.' email-hash:'.hash('sha256', (string) $originalEmail).' summary:'.json_encode($summary));
|
||||
}
|
||||
}
|
||||
|
|
@ -316,12 +316,15 @@ class IncentiveTracker
|
|||
|
||||
/**
|
||||
* Update ranking for all participants of an incentive.
|
||||
* Der Rang entspricht der Anzeigereihenfolge des Leaderboards: Qualifizierte zuerst (1..n),
|
||||
* dahinter Nicht-Qualifizierte, jeweils nach Punkten absteigend.
|
||||
*/
|
||||
public static function updateRanking(Incentive $incentive): void
|
||||
{
|
||||
// Nur Teilnehmer mit Punkten bekommen einen Rang (bei Punktgleichstand: Teilnahme bestaetigt vor anonym)
|
||||
$with_points = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('total_points', '>', 0)
|
||||
->orderByDesc('is_qualified')
|
||||
->orderByDesc('total_points')
|
||||
->orderByRaw('accepted_terms_at IS NOT NULL DESC')
|
||||
->get();
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
<?php
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use Request;
|
||||
use App\Models\UserShop;
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Http\Controllers\Api\KasSLLController;
|
||||
|
||||
class DomainSSL
|
||||
{
|
||||
|
||||
public static function show()
|
||||
{
|
||||
$user_shops = UserShop::limit(1000)->get();
|
||||
$text = "";
|
||||
|
||||
$kas = new KasController();
|
||||
//$domain = 'mivita.care';
|
||||
// $ssl = KasSLLController::getApiSSLParameter();
|
||||
$SubDomains = [];
|
||||
|
||||
$get_subdomains = $kas->action('get_subdomains');
|
||||
foreach ($get_subdomains as $subdomain){
|
||||
if(isset($subdomain['subdomain_name'])){
|
||||
if(strpos($subdomain['subdomain_name'], 'www.') !== false){
|
||||
continue;
|
||||
}
|
||||
if(strpos($subdomain['subdomain_name'], 'api.') !== false){
|
||||
continue;
|
||||
}
|
||||
if(strpos($subdomain['subdomain_name'], 'checkout.') !== false){
|
||||
continue;
|
||||
}
|
||||
$SubDomains[$subdomain['subdomain_name']] =
|
||||
[
|
||||
'ssl_certificate_sni' => $subdomain['ssl_certificate_sni'],
|
||||
'php_version' => $subdomain['php_version'],
|
||||
];
|
||||
|
||||
}else{
|
||||
@dump($subdomain);
|
||||
}
|
||||
|
||||
|
||||
/* if($subdomain['ssl_certificate_sni'] !== "Y"){
|
||||
$pra = array(
|
||||
'hostname' => $subdomain['subdomain_name'],
|
||||
);
|
||||
$pra = array_merge($pra, $ssl);
|
||||
$value = $kas->action('update_ssl', $pra);
|
||||
$text .= $value;
|
||||
}else{
|
||||
if(isset($subdomain['ssl_certificate_sni_is_active'])){
|
||||
$text .= $subdomain['ssl_certificate_sni_is_active'].'-is_active';
|
||||
}else{
|
||||
$text .= '-CHECK!';
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
//$text .= "\n";
|
||||
}
|
||||
|
||||
$dobbleDomains = [];
|
||||
|
||||
foreach($user_shops as $user_shop){
|
||||
$user_shop->hasSubdomain = false;
|
||||
$user_shop->hasSSL = false;
|
||||
$user_shop->PHPversion = "";
|
||||
if(array_key_exists($user_shop->slug.'.mivita.care', $SubDomains)){
|
||||
$user_shop->hasSubdomain = true;
|
||||
$user_shop->hasSSL = $SubDomains[$user_shop->slug.'.mivita.care']['ssl_certificate_sni'] === 'Y' ? true : false;
|
||||
$user_shop->PHPversion = $SubDomains[$user_shop->slug.'.mivita.care']['php_version'];
|
||||
unset($SubDomains[$user_shop->slug.'.mivita.care']);
|
||||
}
|
||||
$dobbleDomains[$user_shop->user_id][$user_shop->id] = $user_shop->slug.'.mivita.care';
|
||||
}
|
||||
foreach($dobbleDomains as $key => $dobbleDomain){
|
||||
if(count($dobbleDomain) === 1){
|
||||
unset($dobbleDomains[$key]);
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'values' => $user_shops,
|
||||
'text' => $text,
|
||||
'SubDomains' => $SubDomains,
|
||||
'dobbleDomains' => $dobbleDomains,
|
||||
|
||||
];
|
||||
|
||||
return view('sys.tools.domain-ssl', $data);
|
||||
}
|
||||
|
||||
|
||||
public static function store()
|
||||
{
|
||||
$data = Request::all();
|
||||
if(isset($data['delete_sub_kas'])){
|
||||
$kas = new KasController();
|
||||
$pra = array(
|
||||
'subdomain_name' => $data['delete_sub_kas'],
|
||||
);
|
||||
$value = $kas->action('delete_subdomain', $pra);
|
||||
\Session()->flash('alert-success', 'subdomain: '.$value.' gelöscht');
|
||||
|
||||
}
|
||||
if(isset($data['delete_user_shop'])){
|
||||
$user_shop = UserShop::findOrFail($data['delete_user_shop']);
|
||||
$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();
|
||||
$kas = new KasController();
|
||||
$pra = array(
|
||||
'subdomain_name' => $subdomain_name,
|
||||
);
|
||||
$value = $kas->action('delete_subdomain', $pra);
|
||||
\Session()->flash('alert-success', 'shop/subdomain: '.$value.' gelöscht');
|
||||
|
||||
}
|
||||
|
||||
if(isset($data['update_ssl'])){
|
||||
|
||||
//prüfen ob subdomain existiert
|
||||
$user_shop = UserShop::findOrFail($data['update_ssl']);
|
||||
$subdomain_name = $user_shop->slug.'.mivita.care';
|
||||
|
||||
$kas = new KasController();
|
||||
$subdomain = $kas->action('get_subdomains', ['subdomain_name' => $subdomain_name]);
|
||||
if(!isset($subdomain[0]['subdomain_name'])){ //not found
|
||||
self::makeNewSubDomain($user_shop->slug);
|
||||
\Session()->flash('alert-danger', 'subdomain: '.$subdomain_name.' nicht gefunden');
|
||||
return back();
|
||||
}else{
|
||||
self::updateSubDomain($subdomain_name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
private static function updateSubDomain($subdomain_name){
|
||||
|
||||
$kas = new KasController();
|
||||
$ssl = KasSLLController::getApiSSLParameter();
|
||||
$pra = array(
|
||||
'hostname' => $subdomain_name
|
||||
);
|
||||
$pra = array_merge($pra, $ssl);
|
||||
$value = $kas->action('update_ssl', $pra);
|
||||
\Session()->flash('alert-success', 'update SSL: '.$value.'');
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function makeNewSubDomain($slug){
|
||||
|
||||
$kas = new KasController();
|
||||
$domain = 'mivita.care';
|
||||
|
||||
//check if exisist
|
||||
$subdomains = $kas->action('get_subdomains');
|
||||
foreach ($subdomains as $subdomain){
|
||||
if(!isset($subdomain['subdomain_name'])){
|
||||
continue;
|
||||
}
|
||||
$sub = str_replace(".".$domain, '', $subdomain['subdomain_name']);
|
||||
if($sub == $slug){
|
||||
\Session()->flash('alert-danger', __('shop.error_subdomain_exists'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//add
|
||||
$full_subdomain_name = $slug.".".$domain;
|
||||
$pra = array(
|
||||
'subdomain_name' => $slug,
|
||||
'domain_name' => $domain,
|
||||
'subdomain_path' => '/mein.mivita.care/public/',
|
||||
'php_version' => config('app.php_version'),
|
||||
//'ssl_proxy' => 'Y',
|
||||
//'redirect_status' => 0
|
||||
);
|
||||
$add_subdomain = $kas->action('add_subdomain', $pra);
|
||||
if($add_subdomain == $full_subdomain_name){
|
||||
\Session()->flash('alert-success', 'Add Subdomain: '.$add_subdomain.'');
|
||||
return $add_subdomain;
|
||||
}
|
||||
\Session()->flash('alert-danger', 'Fehler: '.$add_subdomain.'');
|
||||
return $add_subdomain;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use Request;
|
||||
use App\Services\Shop;
|
||||
use App\Models\UserShop;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\ShoppingUserService;
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Http\Controllers\Api\KasSLLController;
|
||||
use Request;
|
||||
|
||||
class ShoppingOrders
|
||||
{
|
||||
|
||||
public static function show()
|
||||
{
|
||||
abort(403, 'STOP funtion not online');
|
||||
|
|
@ -21,32 +17,31 @@ class ShoppingOrders
|
|||
'values' => $shopping_users,
|
||||
'text' => '',
|
||||
];
|
||||
|
||||
return view('sys.tools.shopping-orders', $data);
|
||||
}
|
||||
|
||||
|
||||
public static function store()
|
||||
{
|
||||
abort(403, 'STOP funtion not online');
|
||||
dd("");
|
||||
|
||||
dd('');
|
||||
|
||||
$data = Request::all();
|
||||
|
||||
if($data['action'] === 'first_run'){
|
||||
$shopping_users = ShoppingUser::whereHas('shopping_order', function($q) {
|
||||
|
||||
if ($data['action'] === 'first_run') {
|
||||
$shopping_users = ShoppingUser::whereHas('shopping_order', function ($q) {
|
||||
$q->where('txaction', 'paid')->OrWhere('txaction', 'appointed');
|
||||
})->get();
|
||||
|
||||
$order_email = [];
|
||||
$order_number = [];
|
||||
|
||||
foreach ($shopping_users as $shopping_user){
|
||||
foreach ($shopping_users as $shopping_user) {
|
||||
$order_email[$shopping_user->billing_email] = isset($order_email[$shopping_user->billing_email]) ? $order_email[$shopping_user->billing_email] + 1 : 1;
|
||||
if($shopping_user->number) {
|
||||
if ($shopping_user->number) {
|
||||
$order_number[$shopping_user->number] = isset($order_number[$shopping_user->number]) ? $order_number[$shopping_user->number] + 1 : 1;
|
||||
$shopping_user->orders = $order_number[$shopping_user->number];
|
||||
}else {
|
||||
} else {
|
||||
$shopping_user->orders = $order_email[$shopping_user->billing_email];
|
||||
}
|
||||
$shopping_user->save();
|
||||
|
|
@ -55,11 +50,11 @@ class ShoppingOrders
|
|||
\Session()->flash('alert-save', true);
|
||||
}
|
||||
|
||||
if($data['action'] === 'next_run'){
|
||||
if ($data['action'] === 'next_run') {
|
||||
ShoppingUserService::snycOrdersByShoppingUser($shopping_user);
|
||||
\Session()->flash('alert-save', true);
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ class UserService
|
|||
if ($user->account->reverse_charge && $user->account->reverse_charge_valid) {
|
||||
// Rechnungsland ist auch Lieferland, dann RSV
|
||||
if (strtolower($user->account->reverse_charge_code) == strtolower($ShippingCountry->country->code)) {
|
||||
// USt-ID bei jeder Verwendung erneut prüfen (VIES, gecacht)
|
||||
if (! self::verifyReverseChargeVatId($user)) {
|
||||
return false;
|
||||
}
|
||||
self::$user_reverse_charge = true;
|
||||
|
||||
return true;
|
||||
|
|
@ -132,6 +136,49 @@ class UserService
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die hinterlegte USt-ID bei jeder Verwendung erneut.
|
||||
* Deutsche, formal ungültige oder laut VIES ungültige USt-IDs verlieren den
|
||||
* Reverse-Charge-Status; bei VIES-Ausfall gilt der gespeicherte Status weiter.
|
||||
*/
|
||||
public static function verifyReverseChargeVatId(User $user): bool
|
||||
{
|
||||
$vatService = app(VatIdService::class);
|
||||
$vatId = $user->account->tax_identification_number;
|
||||
|
||||
if (! $vatService->qualifiesForReverseCharge($vatId)) {
|
||||
self::revokeReverseCharge($user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $vatService->checkViesCached($vatId, $user->account->country->code ?? null);
|
||||
|
||||
if ($result === VatIdService::CHECK_INVALID) {
|
||||
self::revokeReverseCharge($user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// valid oder VIES nicht erreichbar (Fallback auf gespeicherten Status)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entzieht den Reverse-Charge-Status und informiert den User im Checkout,
|
||||
* dass die Bestellung mit Umsatzsteuer berechnet wird.
|
||||
*/
|
||||
private static function revokeReverseCharge(User $user): void
|
||||
{
|
||||
$user->account->reverse_charge = 0;
|
||||
$user->account->reverse_charge_valid = null;
|
||||
$user->account->save();
|
||||
|
||||
if (app()->bound('request') && request()->hasSession()) {
|
||||
request()->session()->flash('alert-error', __('msg.VATID_no_longer_valid_taxes_applied'));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getYardInfo()
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Api\KasController;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\ShoppingUserMemberLog;
|
||||
use App\Models\UserCleanUpLog;
|
||||
|
|
@ -171,19 +170,10 @@ class UserUtil
|
|||
{
|
||||
// shop wird gelöscht
|
||||
if ($user->shop) {
|
||||
// $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()) {
|
||||
$kas = new KasController();
|
||||
$pra = array(
|
||||
'subdomain_name' => $subdomain_name,
|
||||
);
|
||||
$kas->action('delete_subdomain', $pra);
|
||||
}*/
|
||||
}
|
||||
|
||||
// user soll nicht komplett gelöscht werden
|
||||
|
|
|
|||
173
app/Services/VatIdService.php
Normal file
173
app/Services/VatIdService.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Zentrale Prüfung von Umsatzsteuer-Identifikationsnummern (USt-ID).
|
||||
*
|
||||
* - Formatprüfung (EU-Länderpräfix + Nummernteil)
|
||||
* - Deutsche USt-IDs qualifizieren nie für Reverse-Charge / steuerfreie igL
|
||||
* - Live-Prüfung gegen den EU-VIES-Dienst (mit Cache, da VIES langsam/instabil ist)
|
||||
*/
|
||||
class VatIdService
|
||||
{
|
||||
public const CHECK_VALID = 'valid';
|
||||
|
||||
public const CHECK_INVALID = 'invalid';
|
||||
|
||||
public const CHECK_UNAVAILABLE = 'unavailable';
|
||||
|
||||
private const VIES_WSDL = 'https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl';
|
||||
|
||||
/**
|
||||
* VIES-Ländercodes (EL = Griechenland, XI = Nordirland).
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const EU_COUNTRY_CODES = [
|
||||
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'HR', 'HU',
|
||||
'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'XI',
|
||||
];
|
||||
|
||||
/**
|
||||
* Entfernt Leerzeichen, Punkte, Kommas und Bindestriche und wandelt in Großbuchstaben um.
|
||||
*/
|
||||
public function normalize(?string $vatId): string
|
||||
{
|
||||
return strtoupper(str_replace([' ', '.', '-', ','], '', trim((string) $vatId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Ländercode aus dem USt-ID-Präfix, sonst den Fallback (Account-Land).
|
||||
*/
|
||||
public function extractCountryCode(?string $vatId, ?string $fallbackCountryCode = null): ?string
|
||||
{
|
||||
$normalized = $this->normalize($vatId);
|
||||
$prefix = substr($normalized, 0, 2);
|
||||
|
||||
if (in_array($prefix, self::EU_COUNTRY_CODES, true)) {
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
$fallback = strtoupper((string) $fallbackCountryCode);
|
||||
|
||||
return in_array($fallback, self::EU_COUNTRY_CODES, true) ? $fallback : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Nummernteil ohne Ländercode-Präfix.
|
||||
*/
|
||||
public function extractVatNumber(?string $vatId): string
|
||||
{
|
||||
$normalized = $this->normalize($vatId);
|
||||
|
||||
if (in_array(substr($normalized, 0, 2), self::EU_COUNTRY_CODES, true)) {
|
||||
return substr($normalized, 2);
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grobe Formatprüfung: EU-Länderpräfix + 2-13 Zeichen Nummernteil.
|
||||
*/
|
||||
public function isPlausibleFormat(?string $vatId): bool
|
||||
{
|
||||
$normalized = $this->normalize($vatId);
|
||||
|
||||
return (bool) preg_match('/^[A-Z]{2}[A-Z0-9+*]{2,13}$/', $normalized)
|
||||
&& in_array(substr($normalized, 0, 2), self::EU_COUNTRY_CODES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur ausländische EU-USt-IDs qualifizieren für steuerfreie igL / Reverse-Charge.
|
||||
* Deutsche USt-IDs (DE...) werden grundsätzlich abgelehnt.
|
||||
*/
|
||||
public function qualifiesForReverseCharge(?string $vatId): bool
|
||||
{
|
||||
return $this->isPlausibleFormat($vatId) && ! $this->isGermanVatId($vatId);
|
||||
}
|
||||
|
||||
public function isGermanVatId(?string $vatId): bool
|
||||
{
|
||||
return str_starts_with($this->normalize($vatId), 'DE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Live-Prüfung gegen VIES.
|
||||
*
|
||||
* @return string self::CHECK_VALID | self::CHECK_INVALID | self::CHECK_UNAVAILABLE
|
||||
*/
|
||||
public function checkVies(?string $vatId, ?string $fallbackCountryCode = null): string
|
||||
{
|
||||
if (! $this->isPlausibleFormat($vatId)) {
|
||||
return self::CHECK_INVALID;
|
||||
}
|
||||
|
||||
$countryCode = $this->extractCountryCode($vatId, $fallbackCountryCode);
|
||||
$vatNumber = $this->extractVatNumber($vatId);
|
||||
|
||||
if (! $countryCode || $vatNumber === '') {
|
||||
return self::CHECK_INVALID;
|
||||
}
|
||||
|
||||
if (! config('services.vies.enabled', true)) {
|
||||
return self::CHECK_UNAVAILABLE;
|
||||
}
|
||||
|
||||
$timeout = (int) config('services.vies.timeout', 10);
|
||||
|
||||
try {
|
||||
$client = new \SoapClient(self::VIES_WSDL, [
|
||||
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||
'connection_timeout' => $timeout,
|
||||
'exceptions' => true,
|
||||
'stream_context' => stream_context_create([
|
||||
'http' => ['timeout' => $timeout],
|
||||
]),
|
||||
]);
|
||||
|
||||
$result = $client->checkVat([
|
||||
'countryCode' => $countryCode,
|
||||
'vatNumber' => $vatNumber,
|
||||
]);
|
||||
|
||||
return $result->valid ? self::CHECK_VALID : self::CHECK_INVALID;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('VIES-Prüfung nicht erreichbar', [
|
||||
'vat_id' => $this->normalize($vatId),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return self::CHECK_UNAVAILABLE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VIES-Prüfung mit Cache (24h). CHECK_UNAVAILABLE wird nur kurz gecacht (10 Min.),
|
||||
* damit ein VIES-Ausfall den Checkout nicht bei jedem Aufruf blockiert.
|
||||
*/
|
||||
public function checkViesCached(?string $vatId, ?string $fallbackCountryCode = null): string
|
||||
{
|
||||
if (! $this->isPlausibleFormat($vatId)) {
|
||||
return self::CHECK_INVALID;
|
||||
}
|
||||
|
||||
$cacheKey = 'vies-check:'.$this->normalize($vatId);
|
||||
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$result = $this->checkVies($vatId, $fallbackCountryCode);
|
||||
|
||||
$ttl = $result === self::CHECK_UNAVAILABLE ? now()->addMinutes(10) : now()->addHours(24);
|
||||
Cache::put($cacheKey, $result, $ttl);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
@ -320,7 +320,7 @@ class Yard extends Cart
|
|||
$this->shipping_price = $price;
|
||||
$this->shipping_tax_rate = $shipping_price->tax_rate;
|
||||
$this->shipping_price_net = round($price / ((100 + $shipping_price->tax_rate) / 100), 2);
|
||||
$this->shipping_tax = round($price / (100 + $shipping_price->tax_rate) * 100, 2);
|
||||
$this->shipping_tax = round($price - $this->shipping_price_net, 2);
|
||||
|
||||
$this->putYardExtra('num_comp', $this->num_comp);
|
||||
$this->putYardExtra('shipping_price', $this->shipping_price);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue