gruene-seele/app/Console/Commands/WhitelabelReport.php
Kevin Adametz d687a6c569 White Label: Sofortpaket WL-00 und Bestandsprüfung WL-01
- Etikettgröße im Lieferschein korrekt parsen ("40#3,5" ergab 3 statt 3,5 cm)
- Produktkopie optional inkl. Kundenzuordnungen und Logo-Dateien (zweiter Kopier-Link)
- whitelabel:transfer-assignments: gezielte, wiederholbare Übertragung ohne Dubletten
- whitelabel:migrate-colors: Zuordnungen anhand der Bildfarbe auf die Farbprodukte übertragen (Dry-Run ohne --execute)
- whitelabel:report: rein lesende Bestandsprüfung (fehlende Zuordnungen, Farbwidersprüche, Dateien, Dubletten)
- Migration für attributes.value (Schema-Drift, auf Live No-op)
- Analyse/Entwicklungsplan um Gegenprüfung, systemischen Befund und neue Paketreihenfolge ergänzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:05:42 +00:00

209 lines
9.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Models\Attribute;
use App\Models\Product;
use App\Models\ProductImage;
use App\Models\UserWhitelabelProduct;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* WL-01: Rein lesende Bestandsprüfung der White-Label-Daten.
* Meldet Konflikte mit IDs, ändert nichts.
*/
class WhitelabelReport extends Command
{
protected $signature = 'whitelabel:report
{--since=2026-07-27 : Bestellungen ab diesem Datum prüfen (Anlage der Farbprodukte)}';
protected $description = 'Bestandsprüfung White Label: fehlende Kundenzuordnungen, Farbwidersprüche, fehlende Dateien, Dubletten (nur lesend)';
/** @var array<string, string> */
private array $attributeColors = [];
public function handle(): int
{
$this->attributeColors = Attribute::query()->where('attribute_type_id', 1)->get()
->mapWithKeys(fn (Attribute $a) => [(string) $a->id => $this->normalizeColor(strtok((string) $a->name, ' ') ?: '')])
->all();
$findings = 0;
$findings += $this->ordersWithoutAssignment((string) $this->option('since'));
$findings += $this->colorMismatches();
$findings += $this->customerImagesWithoutMatch();
$findings += $this->missingFiles();
$findings += $this->duplicates();
$findings += $this->productsWithoutOrMultipleStandardLabels();
$this->newLine();
$this->info($findings === 0 ? 'Keine Befunde.' : $findings.' Befund(e). Es wurde nichts geändert.');
return self::SUCCESS;
}
/** Bestellungen mit White-Label-Produkt ohne Kundenzuordnung, obwohl der Partner anderswo Logos hat. */
private function ordersWithoutAssignment(string $since): int
{
$rows = DB::table('shopping_orders as so')
->join('shopping_order_items as soi', 'soi.shopping_order_id', '=', 'so.id')
->join('products as p', 'p.id', '=', 'soi.product_id')
->where('p.whitelabel', true)
->where('so.created_at', '>=', $since)
->whereNull('soi.deleted_at')
->whereNotNull('so.auth_user_id')
->whereIn('so.auth_user_id', fn ($q) => $q->select('user_id')->from('user_whitelabel_products'))
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('user_whitelabel_products as uwp')
->whereColumn('uwp.user_id', 'so.auth_user_id')
->whereColumn('uwp.product_id', 'soi.product_id'))
->select('so.id as order_id', 'so.auth_user_id as user_id', 'soi.product_id', 'p.name', 'so.user_white_label', 'so.created_at')
->distinct()
->orderBy('so.id')
->get();
$this->section('A) Bestellungen ab '.$since.' ohne Kundenzuordnung (Partner hat Logos bei anderen Produkten)', $rows->count());
if ($rows->isNotEmpty()) {
$this->table(['Bestellung', 'Partner', 'Produkt', 'Name', 'user_white_label', 'Datum'],
$rows->map(fn ($r) => [$r->order_id, $r->user_id, $r->product_id, $r->name, $r->user_white_label ? 'ja' : 'nein', substr((string) $r->created_at, 0, 10)])->all());
}
return $rows->count();
}
/** Farbe am Kundenbild (Attribut) widerspricht der Farbe im Produktnamen "(Tiegel X)". */
private function colorMismatches(): int
{
$rows = [];
foreach (UserWhitelabelProduct::with(['product', 'whitelabel_images'])->orderBy('id')->get() as $assignment) {
$productColor = $this->productColor($assignment->product);
if ($productColor === null) {
continue;
}
foreach ($assignment->whitelabel_images as $image) {
$imageColors = collect((array) $image->attributes)
->map(fn ($id) => $this->attributeColors[(string) $id] ?? null)
->filter()->unique()->values();
if ($imageColors->isNotEmpty() && ! $imageColors->contains($productColor)) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, $assignment->product->name, $image->id, $imageColors->join(', ')];
}
}
}
$this->section('B) Farbwiderspruch: Kundenbild-Variante passt nicht zur Produktfarbe', count($rows));
if ($rows !== []) {
$this->table(['Zuordnung', 'Partner', 'Produkt', 'Produktname', 'Bild', 'Bildfarbe(n)'], $rows);
}
return count($rows);
}
/** Kundenbilder, die im Lieferschein nie greifen: ohne Variante oder ohne Überschneidung mit dem Standardetikett. */
private function customerImagesWithoutMatch(): int
{
$rows = [];
foreach (UserWhitelabelProduct::with(['product.whitelabel_images', 'whitelabel_images'])->orderBy('id')->get() as $assignment) {
if (! $assignment->product) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, '-', 'Produkt existiert nicht'];
continue;
}
$standardAttributes = $assignment->product->whitelabel_images
->flatMap(fn ($img) => (array) $img->attributes)->map(fn ($v) => (string) $v)->unique();
foreach ($assignment->whitelabel_images as $image) {
$own = collect((array) $image->attributes)->map(fn ($v) => (string) $v);
if ($own->isEmpty()) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, $image->id, 'keine Variante am Kundenbild'];
} elseif ($standardAttributes->isEmpty()) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, $image->id, 'Produkt hat kein Standardetikett'];
} elseif ($own->intersect($standardAttributes)->isEmpty()) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, $image->id, 'keine Überschneidung mit Standardetikett ('.$own->join(',').' vs. '.$standardAttributes->join(',').')'];
}
}
if ($assignment->whitelabel_images->isEmpty()) {
$rows[] = [$assignment->id, $assignment->user_id, $assignment->product_id, '-', 'Zuordnung ohne Kundenbild'];
}
}
$this->section('C) Kundenzuordnungen, die im Lieferschein nicht greifen', count($rows));
if ($rows !== []) {
$this->table(['Zuordnung', 'Partner', 'Produkt', 'Bild', 'Grund'], $rows);
}
return count($rows);
}
private function missingFiles(): int
{
$rows = [];
foreach (ProductImage::query()->whereIn('type', ['uwllogo', 'wllogo'])->orderBy('id')->get() as $image) {
if (! file_exists($image->getBaseImagePath())) {
$rows[] = [$image->id, $image->type, $image->product_id ?? '-', $image->user_wl_product_id ?? '-', $image->getImagePath()];
}
}
$this->section('D) Etikettbilder ohne Datei', count($rows));
if ($rows !== []) {
$this->table(['Bild', 'Typ', 'Produkt', 'Zuordnung', 'Pfad'], $rows);
}
return count($rows);
}
private function duplicates(): int
{
$rows = DB::table('user_whitelabel_products')
->select('user_id', 'product_id', DB::raw('count(*) as c'))
->groupBy('user_id', 'product_id')->having('c', '>', 1)->get();
$this->section('E) Dubletten (user_id, product_id)', $rows->count());
if ($rows->isNotEmpty()) {
$this->table(['Partner', 'Produkt', 'Anzahl'], $rows->map(fn ($r) => [$r->user_id, $r->product_id, $r->c])->all());
}
return $rows->count();
}
private function productsWithoutOrMultipleStandardLabels(): int
{
$rows = [];
foreach (Product::query()->where('whitelabel', true)->withCount('whitelabel_images')->orderBy('id')->get() as $product) {
if ($product->whitelabel_images_count === 0) {
$rows[] = [$product->id, $product->name, 'kein Standardetikett'];
} elseif ($product->whitelabel_images_count > 1) {
$rows[] = [$product->id, $product->name, $product->whitelabel_images_count.' Standardetiketten (Mehrfachetikett prüfen)'];
}
if ($this->productColor($product) === null) {
$rows[] = [$product->id, $product->name, 'keine Farbe im Produktnamen erkennbar'];
}
}
$this->section('F) White-Label-Produkte mit fehlendem/mehrfachem Standardetikett oder ohne erkennbare Farbe', count($rows));
if ($rows !== []) {
$this->table(['Produkt', 'Name', 'Befund'], $rows);
}
return count($rows);
}
private function productColor(?Product $product): ?string
{
if (! $product || ! preg_match('/\(Tiegel\s+([^)]+)\)/iu', (string) $product->name, $m)) {
return null;
}
return $this->normalizeColor($m[1]);
}
private function normalizeColor(string $value): string
{
return str_replace(['ß', 'ss'], 'ss', mb_strtolower(trim($value)));
}
private function section(string $title, int $count): void
{
$this->newLine();
$this->line('<comment>'.$title.'</comment> – '.$count);
}
}