user_abo_items as $item) { if ($item->comp) { continue; } if ($item->product) { $weight += (int) $item->product->weight * (int) $item->qty; } } foreach ($user_abo->one_time_items()->get() as $item) { $product = Product::find($item->product_id); if ($product) { $weight += (int) $product->weight * (int) $item->qty; } } return $weight; } /** * Prüft, ob das kombinierte Abo-Gewicht zzgl. eines Zusatzgewichts das Maximalgewicht * des Versandlandes überschreiten würde. Setzt einen über initYard() initialisierten Yard voraus. */ public static function exceedsMaxWeight(UserAbo $user_abo, int $additionalWeight = 0): bool { $maxWeight = Yard::instance(self::INSTANCE)->getMaxWeight(); if ($maxWeight <= 0) { return false; } return (self::combinedWeight($user_abo) + $additionalWeight) > $maxWeight; } public static function initYard($user_abo) { // WICHTIG: Statische Variablen zurücksetzen, um sicherzustellen, dass keine Daten // aus vorherigen Abos verwendet werden self::$user_abo = $user_abo; self::$is_for = null; self::$customer_detail = null; // Sicherstellen, dass UserService den Abo-Yard initialisiert (nicht 'shopping') UserService::setInstance(self::INSTANCE); // Yard komplett leeren - wichtig für Batch-Verarbeitung mehrerer Abos $yard = Yard::instance(self::INSTANCE); $itemsBeforeDestroy = $yard->content()->count(); $yard->destroy(); \Log::info('AboOrderCart::initYard: Yard geleert', [ 'abo_id' => $user_abo->id, 'items_vor_destroy' => $itemsBeforeDestroy, ]); $itemsAfterDestroy = $yard->content()->count(); \Log::info('AboOrderCart::initYard: Yard geleert', [ 'abo_id' => $user_abo->id, 'items_after_destroy' => $itemsAfterDestroy, ]); self::$customer_detail = self::makeCustomerDetail($user_abo); if ($user_abo->is_for === 'me') { self::$is_for = 'abo-me'; if ($user_abo->user && $user_abo->user->account->same_as_billing) { $country_id = $user_abo->user->account->country_id; } else { $country_id = $user_abo->user->account->shipping_country_id; } if ($country_id && $shipping_country = ShippingCountry::whereCountryId($country_id)->first()) { if ($shipping_country->shipping && $shipping_country->shipping->active) { UserService::initUserYard($user_abo->user, $shipping_country->id, 'abo-me'); return true; } } abort(403, 'Fehler: Versandland nicht gefunden'); } if ($user_abo->is_for === 'ot') { self::$is_for = 'abo-ot-customer'; UserService::initCustomerYard(self::$customer_detail, 'abo-ot-customer'); return true; } return false; } public static function makeOrderYard($user_abo) { // WICHTIG: Statische Variablen explizit setzen für dieses Abo self::$user_abo = $user_abo; if ($user_abo->is_for === 'ot') { self::$is_for = 'abo-ot-customer'; } if ($user_abo->is_for === 'me') { self::$is_for = 'abo-me'; } // WICHTIG: Yard IMMER leeren, um sicherzustellen, dass keine Produkte aus vorherigen Aufrufen vorhanden sind // Dies ist besonders wichtig bei wiederholten Aufrufen der detail-Funktion (z.B. durch AJAX-Requests) $yard = Yard::instance(self::INSTANCE); $itemsBefore = $yard->content()->count(); $yard->destroy(); if ($itemsBefore > 0) { \Log::warning('AboOrderCart::makeOrderYard: Yard war nicht leer vor makeOrderYard und wurde geleert', [ 'abo_id' => $user_abo->id, 'items_before' => $itemsBefore, ]); } AboHelper::ensureUserAboItemsFromLatestOrder($user_abo); // Sicherstellen, dass die Items für dieses spezifische Abo geladen werden // Verwende fresh() um sicherzustellen, dass wir die aktuellen Daten haben $abo_items = $user_abo->user_abo_items()->get(); \Log::info('AboOrderCart::makeOrderYard: Füge Produkte zum Cart hinzu', [ 'abo_id' => $user_abo->id, 'item_count' => $abo_items->count(), 'items' => $abo_items->map(function ($item) { return [ 'id' => $item->id, 'product_id' => $item->product_id, 'qty' => $item->qty, 'comp' => $item->comp, ]; })->toArray(), ]); foreach ($abo_items as $abo_item) { self::addProductToCart($abo_item); } Yard::instance(self::INSTANCE)->reCalculateShippingPrice(); $user_abo->amount = Yard::instance(self::INSTANCE)->totalWithShipping(2, '.', '') * 100; $user_abo->save(); } private static function addProductToCart($item) { $product = Product::find($item->product_id); $tax_free = Yard::instance(self::INSTANCE)->getUserTaxFree(); $user_country = Yard::instance(self::INSTANCE)->getUserCountry(); if ($product) { if ($item->comp) { $cartItem = Yard::instance(self::INSTANCE)->add( $product->id, $product->getLang('name'), 1, 0, false, false, [ 'image' => '', 'slug' => $product->slug, 'weight' => 0, 'points' => 0, 'comp' => $item->comp, 'product_id' => $product->id, ] ); Yard::setTax($cartItem->rowId, 0); return true; } if (self::$is_for === 'ot-customer' || self::$is_for === 'abo-ot-customer') { $cartItem = Yard::instance(self::INSTANCE) ->add( $product->id, $product->getLang('name'), $item->qty, round($product->getPriceWith($tax_free, false, $user_country, false, self::$user_abo->user), 1), false, false, ['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on] ); } else { $cartItem = Yard::instance(self::INSTANCE) ->add( $product->id, $product->getLang('name'), $item->qty, $product->getPriceWith($tax_free, true, $user_country, false, self::$user_abo->user), false, false, ['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on] ); } if ($tax_free) { Yard::setTax($cartItem->rowId, 0); } else { Yard::setTax($cartItem->rowId, $product->getTaxWith($user_country)); } } } /** * Lädt die einmalig hinzugefügten Produkte (normales Bestellsortiment) zusätzlich * zu den Abo-Produkten in denselben Yard – mit dem eingefrorenen Snapshot-Preis und * der Cart-Option `abo_addon = true`. Anschließend wird der Versand über das * Gesamtgewicht neu berechnet. * * Wichtig: Diese Methode verändert NICHT `user_abos.amount` (reiner Abo-Betrag). * Sie muss nach {@see self::makeOrderYard()} aufgerufen werden. */ public static function addOneTimeItemsToYard(UserAbo $user_abo): void { self::$user_abo = $user_abo; self::$is_for = $user_abo->is_for === 'me' ? 'abo-me' : 'abo-ot-customer'; $yard = Yard::instance(self::INSTANCE); // Nur verbindlich bestätigte Einmal-Artikel fließen in die nächste Lieferung // (Snapshot-Menge == bestätigte Menge). Unbestätigte Entwürfe bleiben außen vor. $confirmedItems = $user_abo->one_time_items() ->whereNotNull('confirmed_at') ->whereColumn('confirmed_qty', 'qty') ->get(); foreach ($confirmedItems as $item) { $product = Product::find($item->product_id); if (! $product) { continue; } $cartItem = $yard->add( $product->id, $product->getLang('name'), $item->qty, (float) $item->price, false, false, [ 'image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $item->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on, 'comp' => 0, 'abo_addon' => true, 'one_time_item_id' => $item->id, 'product_id' => $product->id, ] ); Yard::setTax($cartItem->rowId, (float) $item->tax_rate); } $yard->reCalculateShippingPrice(); } /** * Erstellt einen Preis-Snapshot für ein einmalig hinzugefügtes Produkt, konsistent * zur Cart-Preisberechnung in {@see self::addProductToCart()}. * * @return array{price: float, price_net: float, tax_rate: float, tax: float, price_vk_net: float, discount: float, points: int} */ public static function buildOneTimeSnapshot(Product $product, UserAbo $user_abo): array { $yard = Yard::instance(self::INSTANCE); $tax_free = $yard->getUserTaxFree(); $country = $yard->getUserCountry(); $is_me = $user_abo->is_for === 'me'; if ($is_me) { $price = (float) $product->getPriceWith($tax_free, true, $country, false, $user_abo->user); } else { $price = round((float) $product->getPriceWith($tax_free, false, $country, false, $user_abo->user), 1); } $tax_rate = $tax_free ? 0.0 : (float) $product->getTaxWith($country); $price_net = $tax_rate > 0 ? round($price / ((100 + $tax_rate) / 100), 3) : round($price, 3); $tax = round($price - $price_net, 3); $price_vk_net = round((float) $product->getPriceWith(true, false, $country), 3); $discount = 0.0; if ($is_me && ! $product->no_commission && $user_abo->user && $user_abo->user->user_level) { $discount = (float) $user_abo->user->user_level->getFormattedMargin(); } return [ 'price' => round($price, 2), 'price_net' => $price_net, 'tax_rate' => $tax_rate, 'tax' => $tax, 'price_vk_net' => $price_vk_net, 'discount' => $discount, 'points' => (int) $product->points, ]; } /** * Liefert getrennte Zwischensummen für Abo-Produkte und einmalig hinzugefügte * Artikel sowie die kombinierten Gesamtwerte (Versand nach Gesamtgewicht). * * @return array{ * abo: array{net: float, gross: float, tax: float}, * one_time: array{net: float, gross: float, tax: float}, * shipping_net: float, * shipping_gross: float, * total_with_shipping: float, * has_one_time: bool * } */ public static function getSplitSummary(): array { $yard = Yard::instance(self::INSTANCE); $abo = ['net' => 0.0, 'gross' => 0.0, 'tax' => 0.0]; $oneTime = ['net' => 0.0, 'gross' => 0.0, 'tax' => 0.0]; foreach ($yard->content() as $row) { $lineGross = (float) $row->price * $row->qty; $lineNet = $row->taxRate > 0 ? $lineGross / ((100 + $row->taxRate) / 100) : $lineGross; $lineTax = $lineGross - $lineNet; $bucket = ($row->options->abo_addon ?? false) ? 'oneTime' : 'abo'; if ($bucket === 'oneTime') { $oneTime['net'] += $lineNet; $oneTime['gross'] += $lineGross; $oneTime['tax'] += $lineTax; } else { $abo['net'] += $lineNet; $abo['gross'] += $lineGross; $abo['tax'] += $lineTax; } } return [ 'abo' => [ 'net' => round($abo['net'], 2), 'gross' => round($abo['gross'], 2), 'tax' => round($abo['tax'], 2), ], 'one_time' => [ 'net' => round($oneTime['net'], 2), 'gross' => round($oneTime['gross'], 2), 'tax' => round($oneTime['tax'], 2), ], 'shipping_net' => (float) $yard->shippingNet(2, '.', ''), 'shipping_gross' => (float) $yard->shipping(2, '.', ''), 'total_with_shipping' => (float) $yard->totalWithShipping(2, '.', ''), 'has_one_time' => $oneTime['gross'] > 0, ]; } public static function checkNumOfCompProducts($user_abo) { if ($user_abo->is_for === 'me') { $needNumComp = Yard::instance(self::INSTANCE)->getNumComp(); $UserAboItems = UserAboItem::where('user_abo_id', $user_abo->id) ->where('comp', '>', 0) ->orderBy('comp') ->get(); if ($UserAboItems->count() < $needNumComp) { $product = Product::whereActive(true) ->where('shipping_addon', true) ->whereJsonContains('show_on', '12') ->orderBy('pos', 'DESC') ->first(); if (! $product) { return false; } for ($comp = $UserAboItems->count() + 1; $comp <= $needNumComp; $comp++) { $UserAboItem = UserAboItem::create([ 'user_abo_id' => $user_abo->id, 'product_id' => $product->id, 'comp' => $comp, 'qty' => 1, 'status' => 1, ]); AboItemHistoryService::logSystemCompAdded($user_abo, $UserAboItem); self::addProductToCart($UserAboItem); } } if ($UserAboItems->count() > $needNumComp) { foreach ($UserAboItems as $UserAboItem) { if ($UserAboItem->comp > $needNumComp) { AboItemHistoryService::logSystemCompRemoved($user_abo, $UserAboItem); $UserAboItem->delete(); } } foreach (Yard::instance(self::INSTANCE)->content() as $row) { if (($row->options->comp ?? 0) > $needNumComp) { Yard::instance(self::INSTANCE)->remove($row->rowId); } } } } } public static function getCustomerDetail() { return self::$customer_detail; } /* Need this, can change the address */ public static function makeCustomerDetail($user_abo) { if ($user_abo->is_for === 'me') { // only on Abo! $user = $user_abo->user; // WICHTIG: Wenn bereits ein shopping_user existiert, diesen replizieren um alle Felder zu behalten // Ansonsten neues Objekt erstellen if ($user_abo->shopping_user) { $shopping_user = $user_abo->shopping_user->replicate(); \Log::info('AboOrderCart::makeCustomerDetail: ShoppingUser repliziert für Abo ID: '.$user_abo->id, [ 'abo_id' => $user_abo->id, 'original_shopping_user_id' => $user_abo->shopping_user->id, ]); } else { $shopping_user = new ShoppingUser; \Log::info('AboOrderCart::makeCustomerDetail: Neuer ShoppingUser erstellt für Abo ID: '.$user_abo->id); } // Account-Daten überschreiben/aktualisieren $shopping_user->billing_salutation = $user->account->salutation; $shopping_user->billing_company = $user->account->company; $shopping_user->billing_firstname = $user->account->first_name; $shopping_user->billing_lastname = $user->account->last_name; $shopping_user->billing_address = $user->account->address; $shopping_user->billing_address_2 = $user->account->address_2; $shopping_user->billing_zipcode = $user->account->zipcode; $shopping_user->billing_city = $user->account->city; $shopping_user->billing_country_id = $user->account->country_id; $shopping_user->billing_phone = $user->account->phone; $shopping_user->billing_email = $user->email ?? null; $shopping_user->language = $user->account->getLocale(); // Auth User ID setzen falls noch nicht gesetzt if (! $shopping_user->auth_user_id) { $shopping_user->auth_user_id = $user->id; } if ($user->account->same_as_billing) { $shopping_user->shipping_salutation = $user->account->salutation; $shopping_user->shipping_company = $user->account->company; $shopping_user->shipping_firstname = $user->account->first_name; $shopping_user->shipping_lastname = $user->account->last_name; $shopping_user->shipping_address = $user->account->address; $shopping_user->shipping_address_2 = $user->account->address_2; $shopping_user->shipping_zipcode = $user->account->zipcode; $shopping_user->shipping_city = $user->account->city; $shopping_user->shipping_country_id = $user->account->country_id; $shopping_user->shipping_phone = $user->account->phone; $shopping_user->shipping_postnumber = $user->account->shipping_postnumber; $shopping_user->same_as_billing = 1; } else { $shopping_user->shipping_salutation = $user->account->shipping_salutation; $shopping_user->shipping_company = $user->account->shipping_company; $shopping_user->shipping_firstname = $user->account->shipping_firstname; $shopping_user->shipping_lastname = $user->account->shipping_lastname; $shopping_user->shipping_address = $user->account->shipping_address; $shopping_user->shipping_address_2 = $user->account->shipping_address_2; $shopping_user->shipping_zipcode = $user->account->shipping_zipcode; $shopping_user->shipping_city = $user->account->shipping_city; $shopping_user->shipping_country_id = $user->account->shipping_country_id; $shopping_user->shipping_phone = $user->account->shipping_phone; $shopping_user->shipping_postnumber = $user->account->shipping_postnumber; $shopping_user->same_as_billing = 0; } } if ($user_abo->is_for === 'ot') { // look for the primary user of this abo $shopping_user = $user_abo->shopping_user->replicate(); } return $shopping_user; } }