toString(); $actor = self::resolveActor(); foreach ($userAbo->user_abo_items as $item) { $product = $item->product; $unitPrice = self::resolveItemPrice($item); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => UserAboItemHistory::ACTION_INITIAL, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_before' => null, 'qty_after' => $item->qty, 'comp' => $item->comp ?? 0, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => $channel, 'batch_id' => $batchId, 'is_initial' => true, ]); } } /** * Log: Produkt hinzugefügt oder Menge erhöht (bei bereits vorhandenem Produkt). */ public static function logProductAdded(UserAbo $userAbo, UserAboItem $item, int $qtyBefore, string $view): void { $product = $item->product; $unitPrice = self::resolveItemPrice($item); $actor = self::resolveActor(); $channel = self::resolveChannel($view); $action = $qtyBefore === 0 ? UserAboItemHistory::ACTION_ADDED : UserAboItemHistory::ACTION_QTY_CHANGED; UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => $action, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_before' => $qtyBefore > 0 ? $qtyBefore : null, 'qty_after' => $item->qty, 'comp' => $item->comp ?? 0, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => $channel, ]); } /** * Log: Menge eines Produkts geändert. */ public static function logQtyChanged(UserAbo $userAbo, UserAboItem $item, int $qtyBefore, int $qtyAfter, string $view): void { if ($qtyBefore === $qtyAfter) { return; } $product = $item->product; $unitPrice = self::resolveItemPrice($item); $actor = self::resolveActor(); $channel = self::resolveChannel($view); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => UserAboItemHistory::ACTION_QTY_CHANGED, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $qtyAfter, 'qty_before' => $qtyBefore, 'qty_after' => $qtyAfter, 'comp' => $item->comp ?? 0, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => $channel, ]); } /** * Log: Produkt entfernt (vor dem delete aufrufen!). */ public static function logProductRemoved(UserAbo $userAbo, UserAboItem $item, string $view): void { $product = $item->product; $unitPrice = self::resolveItemPrice($item); $actor = self::resolveActor(); $channel = self::resolveChannel($view); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => UserAboItemHistory::ACTION_REMOVED, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_before' => $item->qty, 'qty_after' => 0, 'comp' => $item->comp ?? 0, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => $channel, ]); } /** * Log: Comp-Produkt getauscht. */ public static function logCompProductChanged(UserAbo $userAbo, UserAboItem $item, Product $oldProduct, Product $newProduct, string $view): void { // Item hat bereits das neue Produkt — getPrice() liefert den korrekten Anzeige-Preis $unitPrice = self::resolveItemPrice($item); $actor = self::resolveActor(); $channel = self::resolveChannel($view); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $newProduct->id, 'action' => UserAboItemHistory::ACTION_COMP_CHANGED, 'product_name' => $newProduct->getLang('name'), 'product_number' => $newProduct->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'old_product_id' => $oldProduct->id, 'old_product_name' => $oldProduct->getLang('name'), 'comp' => $item->comp, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => $channel, ]); } /** * Log: System hat Comp-Produkt hinzugefügt. */ public static function logSystemCompAdded(UserAbo $userAbo, UserAboItem $item): void { $product = $item->product; $unitPrice = self::resolveItemPrice($item); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => UserAboItemHistory::ACTION_COMP_ADDED, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_after' => $item->qty, 'comp' => $item->comp, 'changed_by_user_id' => null, 'changed_by_name' => 'System', 'channel' => UserAboItemHistory::CHANNEL_SYSTEM, ]); } /** * Log: System hat Comp-Produkt entfernt (vor dem delete aufrufen!). */ public static function logSystemCompRemoved(UserAbo $userAbo, UserAboItem $item): void { $product = $item->product; $unitPrice = self::resolveItemPrice($item); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $product->id, 'action' => UserAboItemHistory::ACTION_COMP_REMOVED, 'product_name' => $product->getLang('name'), 'product_number' => $product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_before' => $item->qty, 'qty_after' => 0, 'comp' => $item->comp, 'changed_by_user_id' => null, 'changed_by_name' => 'System', 'channel' => UserAboItemHistory::CHANNEL_SYSTEM, ]); } /** * Setzt das Abo auf den Ursprungszustand zurück. */ public static function rollbackToInitial(UserAbo $userAbo): bool { $initialItems = $userAbo->user_abo_item_histories() ->where('is_initial', true) ->get(); if ($initialItems->isEmpty()) { return false; } $batchId = Str::uuid()->toString(); $actor = self::resolveActor(); // Aktuelle Items loggen und löschen foreach ($userAbo->user_abo_items as $item) { $unitPrice = self::resolveItemPrice($item); UserAboItemHistory::create([ 'user_abo_id' => $userAbo->id, 'user_abo_item_id' => $item->id, 'product_id' => $item->product_id, 'action' => UserAboItemHistory::ACTION_ROLLBACK, 'product_name' => $item->product->getLang('name'), 'product_number' => $item->product->number ?? null, 'unit_price' => $unitPrice, 'total_price' => $unitPrice * $item->qty, 'qty_before' => $item->qty, 'qty_after' => 0, 'comp' => $item->comp ?? 0, 'changed_by_user_id' => $actor['id'], 'changed_by_name' => $actor['name'], 'channel' => UserAboItemHistory::CHANNEL_ADMIN, 'batch_id' => $batchId, ]); } // Alle aktuellen Items löschen UserAboItem::where('user_abo_id', $userAbo->id)->delete(); // Neue Items aus Initial-History erstellen foreach ($initialItems as $historyItem) { UserAboItem::create([ 'user_abo_id' => $userAbo->id, 'product_id' => $historyItem->product_id, 'comp' => $historyItem->comp, 'qty' => $historyItem->qty_after, 'status' => 1, ]); } return true; } /** * Ermittelt den exakten Anzeige-Preis des UserAboItems. * Nutzt UserAboItem::getPrice() — das ist genau der Preis, der dem User * in der Produktliste und im Warenkorb angezeigt wird (netto+UserFactor * für Berater, brutto für Kunden, länderspezifisch). * * Fallback-Kette wenn Yard nicht verfügbar: * 1. getPriceWith() ohne Country (netto/ufactor je nach is_for) * 2. Rohpreis des Produkts */ private static function resolveItemPrice(UserAboItem $item): float { // 1. Exakter Anzeige-Preis via UserAboItem::getPrice() (nutzt Yard) try { return (float) $item->getPrice(); } catch (\Throwable $e) { // Yard nicht initialisiert — weiter zum Fallback } // 2. Manuelle Berechnung ohne Yard (ohne Country-spezifische Preise) try { $isMe = $item->user_abo->is_for === 'me'; return (float) $item->product->getPriceWith($isMe, $isMe); } catch (\Throwable $e) { // Letzter Fallback } // 3. Roher Produktpreis return (float) ($item->product->price ?? 0); } /** * Aktuellen Benutzer ermitteln (CRM Auth oder Kunden-Guard). */ private static function resolveActor(): array { // CRM User (Admin/Berater) $user = \Auth::user(); if ($user) { $name = ''; if ($user->account) { $name = trim($user->account->first_name.' '.$user->account->last_name); } if (! $name) { $name = $user->email ?? 'User #'.$user->id; } return ['id' => $user->id, 'name' => $name]; } // Portal Kunde $customer = \Auth::guard('customers')->user(); if ($customer) { return ['id' => null, 'name' => $customer->name ?? $customer->email ?? 'Kunde']; } return ['id' => null, 'name' => 'System']; } /** * Channel aus View-Parameter und Auth-Kontext ableiten. */ private static function resolveChannel(string $view): string { if ($view === 'portal') { return UserAboItemHistory::CHANNEL_PORTAL; } if ($view === 'admin') { return UserAboItemHistory::CHANNEL_ADMIN; } // Bei user views: Prüfen ob Admin die Änderung vornimmt $user = \Auth::user(); if ($user && $user->isAdmin()) { return UserAboItemHistory::CHANNEL_ADMIN; } if ($view === 'me') { return UserAboItemHistory::CHANNEL_USER_ME; } if ($view === 'ot') { return UserAboItemHistory::CHANNEL_USER_OT; } return UserAboItemHistory::CHANNEL_SYSTEM; } }