508 lines
20 KiB
PHP
508 lines
20 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Product;
|
||
use App\Models\ShippingCountry;
|
||
use App\Models\ShoppingUser;
|
||
use App\Models\UserAbo;
|
||
use App\Models\UserAboItem;
|
||
use App\User;
|
||
use Yard;
|
||
|
||
class AboOrderCart
|
||
{
|
||
/**
|
||
* Eigene Warenkorb-Instanz für den Abo-Detail-/Bearbeitungs-Flow.
|
||
* Bewusst getrennt von der regulären Bestell-Instanz ('shopping') und der
|
||
* Abo-Anlage-Instanz ('subscription'), damit sich die Warenkörbe über
|
||
* mehrere Browser-Fenster/Tabs hinweg nicht vermischen.
|
||
*/
|
||
public const INSTANCE = 'abo';
|
||
|
||
private static $user_abo;
|
||
|
||
private static $is_for;
|
||
|
||
private static $customer_detail;
|
||
|
||
/**
|
||
* Gesamtgewicht (in Gramm) aller regulären Abo-Artikel und Einmal-Artikel des Abos.
|
||
* Comp-Produkte zählen nicht (Gewicht 0).
|
||
*/
|
||
public static function combinedWeight(UserAbo $user_abo): int
|
||
{
|
||
$weight = 0;
|
||
foreach ($user_abo->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);
|
||
|
||
foreach ($user_abo->one_time_items()->get() 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;
|
||
}
|
||
}
|