Abo Einmalprodukte und Bestätigung abschließen
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2bdc9ada3c
commit
2269ce031f
57 changed files with 3647 additions and 371 deletions
|
|
@ -163,13 +163,13 @@ class UserMakeOrder
|
|||
|
||||
// WICHTIG: Yard komplett leeren vor jedem Abo, um sicherzustellen, dass keine Produkte
|
||||
// aus vorherigen Abos im Cart bleiben
|
||||
Yard::instance('shopping')->destroy();
|
||||
Yard::instance(AboOrderCart::INSTANCE)->destroy();
|
||||
|
||||
// initYard akzeptiert nur einen Parameter (user_abo)
|
||||
AboOrderCart::initYard($this->userAbo);
|
||||
|
||||
// Nochmalige Sicherheitsprüfung: Yard sollte leer sein
|
||||
$yardBefore = Yard::instance('shopping');
|
||||
$yardBefore = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$itemsBefore = $yardBefore->content();
|
||||
if ($itemsBefore->count() > 0) {
|
||||
Log::warning('UserMakeOrder: Yard war nicht leer nach initYard für Abo ID: '.$this->userAbo->id.', Items: '.$itemsBefore->count());
|
||||
|
|
@ -179,7 +179,7 @@ class UserMakeOrder
|
|||
// hier wird die Bestellung erstellt inkl aktueller Preise
|
||||
AboOrderCart::makeOrderYard($this->userAbo);
|
||||
|
||||
$yard = Yard::instance('shopping');
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
|
||||
// Debug: Logge welche Produkte im Cart sind
|
||||
$items = $yard->content();
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ class ModalController extends Controller
|
|||
$user_abo = UserAbo::find($data['id']);
|
||||
$ret = view('user.abo.modal_abo_show_products', compact('data', 'user_abo'))->render();
|
||||
}
|
||||
if ($data['action'] === 'abo-add-onetime') {
|
||||
$user_abo = UserAbo::find($data['id']);
|
||||
$ret = view('user.abo.modal_abo_onetime_products', compact('data', 'user_abo'))->render();
|
||||
}
|
||||
|
||||
if ($data['action'] === 'create-dhl-shipment') {
|
||||
$this->authorizeDhlShipmentModal();
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@
|
|||
namespace App\Http\Controllers\Portal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Abo\AboOneTimeItemRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingInstance;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Repositories\AboRepository;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\Shop;
|
||||
use App\Services\UserService;
|
||||
|
|
@ -68,6 +71,14 @@ class AboController extends Controller
|
|||
AboOrderCart::initYard($user_abo);
|
||||
$customer_detail = AboOrderCart::getCustomerDetail();
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
|
||||
$oneTimeWindowOpen = AboHelper::isOneTimeWindowOpen($user_abo);
|
||||
if ($oneTimeWindowOpen) {
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo);
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo);
|
||||
}
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
return view('portal.abo.my_abo', [
|
||||
'user_abo' => $user_abo,
|
||||
|
|
@ -75,6 +86,9 @@ class AboController extends Controller
|
|||
'view' => $view,
|
||||
'comp_products' => [],
|
||||
'isAdmin' => false,
|
||||
'one_time_window_open' => $oneTimeWindowOpen,
|
||||
'summary' => $summary,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -98,11 +112,15 @@ class AboController extends Controller
|
|||
|
||||
if (Request::ajax()) {
|
||||
$message = false;
|
||||
// Yard für Gewichts-/Versandprüfung initialisieren (Versandland setzen)
|
||||
AboOrderCart::initYard($user_abo);
|
||||
|
||||
// addProduct
|
||||
if ($data['action'] === 'addProduct') {
|
||||
if ($product = Product::find($data['product_id'])) {
|
||||
if ($UserAboItem = UserAboItem::where('user_abo_id', $user_abo->id)->where('product_id', $product->id)->where('comp', 0)->first()) {
|
||||
if (AboOrderCart::exceedsMaxWeight($user_abo, (int) $product->weight)) {
|
||||
$message = __('msg.cart_max_weight_reached');
|
||||
} elseif ($UserAboItem = UserAboItem::where('user_abo_id', $user_abo->id)->where('product_id', $product->id)->where('comp', 0)->first()) {
|
||||
$qtyBefore = $UserAboItem->qty;
|
||||
$UserAboItem->qty = $UserAboItem->qty + 1;
|
||||
$UserAboItem->save();
|
||||
|
|
@ -132,9 +150,14 @@ class AboController extends Controller
|
|||
if ($isAddOnlyMode && $qty < $UserAboItem->qty) {
|
||||
$qty = $UserAboItem->qty;
|
||||
}
|
||||
$UserAboItem->qty = $qty;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $view);
|
||||
$additionalWeight = (int) $product->weight * ($qty - $qtyBefore);
|
||||
if ($additionalWeight > 0 && AboOrderCart::exceedsMaxWeight($user_abo, $additionalWeight)) {
|
||||
$message = __('msg.cart_max_weight_reached');
|
||||
} else {
|
||||
$UserAboItem->qty = $qty;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,15 +210,32 @@ class AboController extends Controller
|
|||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
if (AboHelper::isOneTimeWindowOpen($user_abo)) {
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo->fresh());
|
||||
}
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo);
|
||||
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
$error_message = $message ? $message : false;
|
||||
$html_cart = view('admin.abo._order_abo_show', ['user_abo' => $user_abo, 'error_message' => $error_message, 'add_only_mode' => $isAddOnlyMode])->render();
|
||||
$html_comp = view('user.order.comp_product', $data)->render();
|
||||
$html_cart = view('admin.abo._order_abo_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'error_message' => $error_message,
|
||||
'split_mode' => AboHelper::isOneTimeWindowOpen($user_abo),
|
||||
'summary' => $summary,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
])->render();
|
||||
$html_comp = view('user.order.comp_product', array_merge($data, [
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
]))->render();
|
||||
$html_summary = view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
])->render();
|
||||
|
||||
$amount = $user_abo->getFormattedAmount();
|
||||
|
||||
return response()->json(['response' => true, 'data' => $data, 'html_cart' => $html_cart, 'html_comp' => $html_comp, 'amount' => $amount]);
|
||||
return response()->json(['response' => true, 'data' => $data, 'html_cart' => $html_cart, 'html_comp' => $html_comp, 'html_summary' => $html_summary, 'amount' => $amount]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -223,8 +263,8 @@ class AboController extends Controller
|
|||
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('add_card', function (Product $product) {
|
||||
$tax_free = Yard::instance('shopping')->getUserTaxFree();
|
||||
$price = $product->getFormattedPriceWith($tax_free, false, Yard::instance('shopping')->getUserCountry());
|
||||
$tax_free = Yard::instance(AboOrderCart::INSTANCE)->getUserTaxFree();
|
||||
$price = $product->getFormattedPriceWith($tax_free, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry());
|
||||
|
||||
return '<button type="button" class="btn btn-sm btn-md-extra btn-secondary add-product-basket" data-product-id="'.$product->id.'" data-product-name="'.e($product->getLang('name')).'" data-product-price="'.$price.' €">
|
||||
<strong>€ '.$price.'</strong> +<span class="ion ion-md-cart"></span>
|
||||
|
|
@ -244,10 +284,10 @@ class AboController extends Controller
|
|||
return '<span class="no-line-break">'.$product->getFormattedPoints().'</span>';
|
||||
})
|
||||
->addColumn('price_net', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, false, Yard::instance('shopping')->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()).'</span>';
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).'</span>';
|
||||
})
|
||||
->addColumn('price_gross', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, false, Yard::instance('shopping')->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()).'</span>';
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).'</span>';
|
||||
})
|
||||
->addColumn('action', function (Product $product) {
|
||||
return '<button class="btn btn-default btn-sm icon-btn md-btn-flat product-tooltip" title="details" data-modal="modal-lg"
|
||||
|
|
@ -282,6 +322,11 @@ class AboController extends Controller
|
|||
$this->checkPortalPermission($user_abo);
|
||||
$ret = view('user.abo.modal_abo_show_products', compact('data', 'user_abo'))->render();
|
||||
}
|
||||
if ($data['action'] === 'abo-add-onetime') {
|
||||
$user_abo = UserAbo::find($data['id']);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
$ret = view('user.abo.modal_abo_onetime_products', compact('data', 'user_abo'))->render();
|
||||
}
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
$user_abo = UserAbo::find($data['id']);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
|
|
@ -301,6 +346,127 @@ class AboController extends Controller
|
|||
abort(404);
|
||||
}
|
||||
|
||||
public function oneTime(AboOneTimeItemRequest $request, $view, $id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $view);
|
||||
|
||||
if (! AboHelper::isOneTimeWindowOpen($user_abo)) {
|
||||
return response()->json([
|
||||
'response' => false,
|
||||
'message' => __('abo.onetime_window_closed'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
$message = (new AboOneTimeService)->handleAction($user_abo, $request->validated());
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo->fresh());
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo);
|
||||
|
||||
$user_abo = $user_abo->fresh();
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
return response()->json([
|
||||
'response' => ! $message,
|
||||
'message' => $message ?: null,
|
||||
'summary' => $summary,
|
||||
'one_time_items' => $this->oneTimeItemsPayload($user_abo),
|
||||
'html_onetime' => view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'summary' => $summary,
|
||||
'error_message' => $message ?: false,
|
||||
])->render(),
|
||||
'html_abo' => view('admin.abo._order_abo_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'split_mode' => true,
|
||||
'summary' => $summary,
|
||||
'only_show_products' => false,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
])->render(),
|
||||
'html_summary' => view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
])->render(),
|
||||
'html_comp' => view('user.order.comp_product', [
|
||||
'comp_products' => [],
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
])->render(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function oneTimeDatatable($user_abo_id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($user_abo_id);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
|
||||
$query = Product::select('products.*')
|
||||
->where('active', true)
|
||||
->whereJsonContains('show_on', '3')
|
||||
->orderBy('name', 'asc');
|
||||
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('add_card', function (Product $product) {
|
||||
$tax_free = Yard::instance(AboOrderCart::INSTANCE)->getUserTaxFree();
|
||||
$price = $product->getFormattedPriceWith($tax_free, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry());
|
||||
|
||||
return '<button type="button" class="btn btn-sm btn-md-extra btn-warning add-onetime-product-basket" data-product-id="'.$product->id.'" data-product-name="'.e($product->getLang('name')).'" data-product-price="'.$price.' €">
|
||||
<strong>€ '.$price.'</strong> +<span class="ion ion-md-cart"></span>
|
||||
</button>';
|
||||
})
|
||||
->addColumn('picture', function (Product $product) {
|
||||
if (count($product->images)) {
|
||||
return '<img class="img-fluid img-extra" alt="" src="'.route('product_image', [$product->images->first()->slug]).'">';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->addColumn('name', function (Product $product) {
|
||||
return '<strong>'.$product->getLang('name').'</strong>';
|
||||
})
|
||||
->addColumn('points', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPoints().'</span>';
|
||||
})
|
||||
->addColumn('price_net', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>';
|
||||
})
|
||||
->addColumn('price_gross', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, false, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>';
|
||||
})
|
||||
->filterColumn('product', function ($query, $keyword) {
|
||||
if ($keyword != '') {
|
||||
$query->where('name', 'LIKE', '%'.$keyword.'%');
|
||||
}
|
||||
})
|
||||
->orderColumn('name', 'name $1')
|
||||
->orderColumn('number', 'number $1')
|
||||
->rawColumns(['add_card', 'points', 'name', 'picture', 'price_net', 'price_gross'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function oneTimeItemsPayload(UserAbo $user_abo): array
|
||||
{
|
||||
return $user_abo->one_time_items()->with('product')->get()->map(function (UserAboOneTimeItem $item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'product_id' => $item->product_id,
|
||||
'name' => $item->product?->getLang('name'),
|
||||
'qty' => $item->qty,
|
||||
'price' => $item->price,
|
||||
'total' => round($item->price * $item->qty, 2),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function checkNeedBasisProduct($user_abo, $product, $order_item_id)
|
||||
{
|
||||
if (AboHelper::getAboShowOn($product) !== 'base') {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,13 @@ class OrderController extends Controller
|
|||
$image = $product->images->first()?->slug ?? '';
|
||||
$yard = Yard::instance($this->instance);
|
||||
|
||||
$additionalWeight = (int) $product->weight * $quantity;
|
||||
if ($additionalWeight > 0 && $yard->exceedsMaxWeight($additionalWeight)) {
|
||||
\Session::flash('alert-error', __('msg.cart_max_weight_reached'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$cartItem = $yard->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Abo\AboOneTimeItemRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Repositories\AboRepository;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\AboItemHistoryService;
|
||||
|
|
@ -85,18 +87,30 @@ class AboController extends Controller
|
|||
// holt die aktuellen UserAccount Daten oder die Userdaten des Abo
|
||||
$customer_detail = AboOrderCart::getCustomerDetail();
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
|
||||
$oneTimeWindowOpen = AboHelper::isOneTimeWindowOpen($user_abo);
|
||||
if ($oneTimeWindowOpen) {
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo);
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo);
|
||||
}
|
||||
|
||||
$comp_products = [];
|
||||
if ($user_abo->is_for === 'me') {
|
||||
$comp_products = Shop::getCompProducts('abo-me');
|
||||
}
|
||||
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
$data = [
|
||||
'user_abo' => $user_abo,
|
||||
'isAdmin' => false,
|
||||
'customer_detail' => $customer_detail,
|
||||
'view' => $view,
|
||||
'comp_products' => $comp_products,
|
||||
'one_time_window_open' => $oneTimeWindowOpen,
|
||||
'summary' => $summary,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
];
|
||||
|
||||
return view('user.abo.detail', $data);
|
||||
|
|
@ -121,10 +135,14 @@ class AboController extends Controller
|
|||
|
||||
if (Request::ajax()) {
|
||||
$message = false;
|
||||
// Yard für Gewichts-/Versandprüfung initialisieren (Versandland setzen)
|
||||
AboOrderCart::initYard($user_abo);
|
||||
// addProduct
|
||||
if ($data['action'] === 'addProduct') {
|
||||
if ($product = Product::find($data['product_id'])) {
|
||||
if ($UserAboItem = UserAboItem::where('user_abo_id', $user_abo->id)->where('product_id', $product->id)->where('comp', 0)->first()) {
|
||||
if (AboOrderCart::exceedsMaxWeight($user_abo, (int) $product->weight)) {
|
||||
$message = __('msg.cart_max_weight_reached');
|
||||
} elseif ($UserAboItem = UserAboItem::where('user_abo_id', $user_abo->id)->where('product_id', $product->id)->where('comp', 0)->first()) {
|
||||
$qtyBefore = $UserAboItem->qty;
|
||||
$UserAboItem->qty = $UserAboItem->qty + 1;
|
||||
$UserAboItem->save();
|
||||
|
|
@ -155,9 +173,14 @@ class AboController extends Controller
|
|||
if ($isAddOnlyMode && $qty < $UserAboItem->qty) {
|
||||
$qty = $UserAboItem->qty;
|
||||
}
|
||||
$UserAboItem->qty = $qty;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $editView);
|
||||
$additionalWeight = (int) $product->weight * ($qty - $qtyBefore);
|
||||
if ($additionalWeight > 0 && AboOrderCart::exceedsMaxWeight($user_abo, $additionalWeight)) {
|
||||
$message = __('msg.cart_max_weight_reached');
|
||||
} else {
|
||||
$UserAboItem->qty = $qty;
|
||||
$UserAboItem->save();
|
||||
AboItemHistoryService::logQtyChanged($user_abo, $UserAboItem, $qtyBefore, $qty, $editView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,23 +232,112 @@ class AboController extends Controller
|
|||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
AboOrderCart::makeOrderYard($user_abo); // reCalculateShippingPrice
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
if (AboHelper::isOneTimeWindowOpen($user_abo)) {
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo->fresh());
|
||||
}
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo); // after reCalculateShippingPrice check it and remove or add comp product
|
||||
|
||||
if ($user_abo->is_for === 'me') {
|
||||
$data['comp_products'] = Shop::getCompProducts('abo-me');
|
||||
}
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
$error_message = $message ? $message : false;
|
||||
$html_cart = view('admin.abo._order_abo_show', ['user_abo' => $user_abo, 'error_message' => $error_message, 'add_only_mode' => $isAddOnlyMode])->render();
|
||||
$html_comp = view('user.order.comp_product', $data)->render();
|
||||
$html_cart = view('admin.abo._order_abo_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'error_message' => $error_message,
|
||||
'split_mode' => AboHelper::isOneTimeWindowOpen($user_abo),
|
||||
'summary' => $summary,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
])->render();
|
||||
$html_comp = view('user.order.comp_product', array_merge($data, [
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
]))->render();
|
||||
$html_summary = view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
])->render();
|
||||
|
||||
$amount = $user_abo->getFormattedAmount();
|
||||
|
||||
// $html_total = view("user.homeparty.show_total_order", ['homeparty' => $homeparty])->render();
|
||||
return response()->json(['response' => true, 'data' => $data, 'html_cart' => $html_cart, 'html_comp' => $html_comp, 'amount' => $amount]);
|
||||
return response()->json(['response' => true, 'data' => $data, 'html_cart' => $html_cart, 'html_comp' => $html_comp, 'html_summary' => $html_summary, 'amount' => $amount]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function oneTime(AboOneTimeItemRequest $request, $view, $id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
$editView = \Auth::user()?->isAdmin() ? 'admin' : $view;
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $editView);
|
||||
|
||||
if (! AboHelper::isOneTimeWindowOpen($user_abo)) {
|
||||
return response()->json([
|
||||
'response' => false,
|
||||
'message' => __('abo.onetime_window_closed'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
$message = (new \App\Services\AboOneTimeService)->handleAction($user_abo, $request->validated());
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
$baseCompCount = Yard::instance(AboOrderCart::INSTANCE)->getNumComp();
|
||||
AboOrderCart::addOneTimeItemsToYard($user_abo->fresh());
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo);
|
||||
|
||||
$user_abo = $user_abo->fresh();
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
$compProducts = $user_abo->is_for === 'me' ? Shop::getCompProducts('abo-me') : [];
|
||||
|
||||
return response()->json([
|
||||
'response' => ! $message,
|
||||
'message' => $message ?: null,
|
||||
'summary' => $summary,
|
||||
'one_time_items' => $this->oneTimeItemsPayload($user_abo),
|
||||
'html_onetime' => view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'summary' => $summary,
|
||||
'error_message' => $message ?: false,
|
||||
])->render(),
|
||||
'html_abo' => view('admin.abo._order_abo_show', [
|
||||
'user_abo' => $user_abo,
|
||||
'split_mode' => true,
|
||||
'summary' => $summary,
|
||||
'only_show_products' => false,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
])->render(),
|
||||
'html_summary' => view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
])->render(),
|
||||
'html_comp' => view('user.order.comp_product', [
|
||||
'comp_products' => $compProducts,
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
'base_comp_count' => $baseCompCount,
|
||||
])->render(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function oneTimeItemsPayload(UserAbo $user_abo): array
|
||||
{
|
||||
return $user_abo->one_time_items()->with('product')->get()->map(function (UserAboOneTimeItem $item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'product_id' => $item->product_id,
|
||||
'name' => $item->product?->getLang('name'),
|
||||
'qty' => $item->qty,
|
||||
'price' => $item->price,
|
||||
'total' => round($item->price * $item->qty, 2),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function check_need_basis_product($user_abo, $product, $order_item_id)
|
||||
{
|
||||
// Wenn das zu entfernende Produkt kein Basis-Produkt ist, keine weitere Prüfung nötig
|
||||
|
|
@ -278,9 +390,9 @@ class AboController extends Controller
|
|||
|
||||
->addColumn('add_card', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
$tax_free = $user_abo->is_for === 'me' ? true : Yard::instance('shopping')->getUserTaxFree();
|
||||
$tax_free = $user_abo->is_for === 'me' ? true : Yard::instance(AboOrderCart::INSTANCE)->getUserTaxFree();
|
||||
|
||||
$price = $product->getFormattedPriceWith($tax_free, $ufactor, Yard::instance('shopping')->getUserCountry());
|
||||
$price = $product->getFormattedPriceWith($tax_free, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry());
|
||||
|
||||
return '<button type="button" class="btn btn-sm btn-md-extra btn-secondary add-product-basket" data-product-id="'.$product->id.'" data-product-name="'.e($product->getLang('name')).'" data-product-price="'.$price.' €">
|
||||
<strong>€ '.$price.'</strong> +<span class="ion ion-md-cart"></span>
|
||||
|
|
@ -302,12 +414,12 @@ class AboController extends Controller
|
|||
->addColumn('price_net', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, $ufactor, Yard::instance('shopping')->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()).'</span>';
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).'</span>';
|
||||
})
|
||||
->addColumn('price_gross', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, $ufactor, Yard::instance('shopping')->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance('shopping')->getUserCountry()).'</span>';
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>'.'<span class="no-line-break">'.$product->getFormattedPriceCurrencyWith(true, true, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).'</span>';
|
||||
})
|
||||
->addColumn('action', function (Product $product) {
|
||||
return '<button class="btn btn-default btn-sm icon-btn md-btn-flat product-tooltip" title="details" data-modal="modal-lg"
|
||||
|
|
@ -332,6 +444,67 @@ class AboController extends Controller
|
|||
->make(true);
|
||||
}
|
||||
|
||||
public function oneTimeDatatable($user_abo_id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($user_abo_id);
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
|
||||
$show_on_ids = $user_abo->is_for === 'me' ? ['2'] : ['3'];
|
||||
$query = Product::select('products.*')
|
||||
->where('active', true)
|
||||
->where(function ($q) use ($show_on_ids) {
|
||||
foreach ($show_on_ids as $id) {
|
||||
$q->orWhereJsonContains('show_on', $id);
|
||||
}
|
||||
})
|
||||
->orderBy('name', 'asc');
|
||||
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('add_card', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
$tax_free = $user_abo->is_for === 'me' ? true : Yard::instance(AboOrderCart::INSTANCE)->getUserTaxFree();
|
||||
|
||||
$price = $product->getFormattedPriceWith($tax_free, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry());
|
||||
|
||||
return '<button type="button" class="btn btn-sm btn-md-extra btn-warning add-onetime-product-basket" data-product-id="'.$product->id.'" data-product-name="'.e($product->getLang('name')).'" data-product-price="'.$price.' €">
|
||||
<strong>€ '.$price.'</strong> +<span class="ion ion-md-cart"></span>
|
||||
</button>';
|
||||
})
|
||||
->addColumn('picture', function (Product $product) {
|
||||
if (count($product->images)) {
|
||||
return '<img class="img-fluid img-extra" alt="" src="'.route('product_image', [$product->images->first()->slug]).'">';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->addColumn('name', function (Product $product) {
|
||||
return '<strong>'.$product->getLang('name').'</strong>';
|
||||
})
|
||||
->addColumn('points', function (Product $product) {
|
||||
return '<span class="no-line-break">'.$product->getFormattedPoints().'</span>';
|
||||
})
|
||||
->addColumn('price_net', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(true, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>';
|
||||
})
|
||||
->addColumn('price_gross', function (Product $product) use ($user_abo) {
|
||||
$ufactor = $user_abo->is_for === 'me' ? true : false;
|
||||
|
||||
return '<span class="no-line-break">'.$product->getFormattedPriceWith(false, $ufactor, Yard::instance(AboOrderCart::INSTANCE)->getUserCountry()).' €</span>';
|
||||
})
|
||||
->filterColumn('product', function ($query, $keyword) {
|
||||
if ($keyword != '') {
|
||||
$query->where('name', 'LIKE', '%'.$keyword.'%');
|
||||
}
|
||||
})
|
||||
->orderColumn('name', 'name $1')
|
||||
->orderColumn('number', 'number $1')
|
||||
->rawColumns(['add_card', 'points', 'name', 'picture', 'price_net', 'price_gross'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
private function checkPermissions($view, $user_abo)
|
||||
{
|
||||
\Log::info('checkPermissions', ['view' => $view, 'user_abo' => $user_abo]);
|
||||
|
|
|
|||
|
|
@ -773,10 +773,11 @@ class OrderController extends Controller
|
|||
$isAbo = str_contains($is_for, 'abo');
|
||||
$qty = isset($data['qty']) ? (int) $data['qty'] : 0;
|
||||
if ($qty > 0 && ! ProductOrderContext::isProductAllowedInContext($product, $isAbo, $is_for)) {
|
||||
return response()->json([
|
||||
'response' => false,
|
||||
'message' => __('msg.cart_product_not_allowed_for_order_type'),
|
||||
]);
|
||||
return $this->cartUpdateRejected($data, __('msg.cart_product_not_allowed_for_order_type'));
|
||||
}
|
||||
|
||||
if ($qty > 0 && $this->exceedsCartMaxWeight(Yard::instance('shopping'), $product, $qty)) {
|
||||
return $this->cartUpdateRejected($data, __('msg.cart_max_weight_reached'));
|
||||
}
|
||||
|
||||
$image = '';
|
||||
|
|
@ -831,6 +832,67 @@ class OrderController extends Controller
|
|||
return response()->json(['response' => true, 'data' => $data, 'html_card' => $html_card, 'html_comp' => $html_comp]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lehnt ein Warenkorb-Update ab, ohne den Warenkorb zu verändern. Der Warenkorb
|
||||
* wird unverändert neu gerendert (Zähler bleibt stehen) und der Hinweis als
|
||||
* Inline-Meldung über dem Warenkorb ausgegeben.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function cartUpdateRejected(array $data, string $message): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$data['error_message'] = $message;
|
||||
|
||||
if (isset($data['product_id'])) {
|
||||
$currentQty = 0;
|
||||
foreach (Yard::instance('shopping')->content() as $row) {
|
||||
if (! $row->options->comp && (int) $row->id === (int) $data['product_id']) {
|
||||
$currentQty = (int) $row->qty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data['qty'] = $currentQty;
|
||||
}
|
||||
|
||||
$html_card = view('user.order.yard_view_form', $data)->render();
|
||||
$html_comp = view('user.order.comp_product', $data)->render();
|
||||
|
||||
return response()->json([
|
||||
'response' => true,
|
||||
'data' => $data,
|
||||
'html_card' => $html_card,
|
||||
'html_comp' => $html_comp,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob das Hochsetzen eines Produkts auf die Zielmenge das Maximalgewicht
|
||||
* des Versandlandes überschreiten würde. Mengenreduktionen werden nie blockiert.
|
||||
*/
|
||||
private function exceedsCartMaxWeight(\App\Services\Yard $yard, Product $product, int $targetQty): bool
|
||||
{
|
||||
$productWeight = (int) $product->weight;
|
||||
if ($productWeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentQty = 0;
|
||||
foreach ($yard->content() as $row) {
|
||||
if (! $row->options->comp && (int) $row->id === (int) $product->id) {
|
||||
$currentQty = (int) $row->qty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$additionalWeight = $productWeight * ($targetQty - $currentQty);
|
||||
if ($additionalWeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $yard->exceedsMaxWeight($additionalWeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updating shipping country
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ class CardController extends Controller
|
|||
{
|
||||
$product = Product::find($id);
|
||||
if ($product && ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
$additionalWeight = (int) $product->weight * (int) $quantity;
|
||||
if ($additionalWeight > 0 && Yard::instance($this->instance)->exceedsMaxWeight($additionalWeight)) {
|
||||
\Session::flash('alert-error', __('msg.cart_max_weight_reached'));
|
||||
|
||||
return back();
|
||||
}
|
||||
$image = '';
|
||||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
|
|
@ -61,11 +67,17 @@ class CardController extends Controller
|
|||
$product = Product::find($id);
|
||||
|
||||
if ($product && ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
$quantity = Request::get('quantity') ? Request::get('quantity') : 1;
|
||||
$additionalWeight = (int) $product->weight * (int) $quantity;
|
||||
if ($additionalWeight > 0 && Yard::instance($this->instance)->exceedsMaxWeight($additionalWeight)) {
|
||||
\Session::flash('alert-error', __('msg.cart_max_weight_reached'));
|
||||
|
||||
return back();
|
||||
}
|
||||
$image = '';
|
||||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
}
|
||||
$quantity = Request::get('quantity') ? Request::get('quantity') : 1;
|
||||
$cartItem = Yard::instance($this->instance)
|
||||
->add(
|
||||
$product->id,
|
||||
|
|
@ -122,6 +134,14 @@ class CardController extends Controller
|
|||
if ($product && $product->is_membership_only) {
|
||||
$qty = 1;
|
||||
}
|
||||
if ($product) {
|
||||
$additionalWeight = (int) $product->weight * ((int) $qty - (int) $cartItem->qty);
|
||||
if ($additionalWeight > 0 && Yard::instance($this->instance)->exceedsMaxWeight($additionalWeight)) {
|
||||
\Session::flash('alert-error', __('msg.cart_max_weight_reached'));
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Yard::instance($this->instance)->update($rowId, $qty);
|
||||
Yard::instance($this->instance)->reCalculateShippingPrice();
|
||||
|
|
|
|||
42
app/Http/Requests/Abo/AboOneTimeItemRequest.php
Normal file
42
app/Http/Requests/Abo/AboOneTimeItemRequest.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Abo;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AboOneTimeItemRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, mixed>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => ['required', 'in:add,update,remove,confirm,discard'],
|
||||
'product_id' => ['required_if:action,add', 'integer', 'exists:products,id'],
|
||||
'one_time_item_id' => ['required_if:action,update,remove', 'integer', 'exists:user_abo_one_time_items,id'],
|
||||
'qty' => ['required_if:action,update', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'action.required' => __('abo.onetime_action_required'),
|
||||
'action.in' => __('abo.onetime_action_invalid'),
|
||||
'product_id.required_if' => __('abo.product_not_found'),
|
||||
'product_id.exists' => __('abo.product_not_found'),
|
||||
'one_time_item_id.required_if' => __('abo.abo_item_not_found'),
|
||||
'one_time_item_id.exists' => __('abo.abo_item_not_found'),
|
||||
'qty.required_if' => __('abo.onetime_qty_required'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Product $product
|
||||
* @property-read \App\Models\ShoppingOrder $shopping_order
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem query()
|
||||
|
|
@ -31,32 +32,43 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereShoppingOrderId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereSlug($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereUpdatedAt($value)
|
||||
*
|
||||
* @property float|null $tax_rate
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property string|null $user_deleted_at
|
||||
*
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrderItem onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereTaxRate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereUserDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrderItem withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\Models\ShoppingOrderItem withoutTrashed()
|
||||
*
|
||||
* @property int|null $comp
|
||||
* @property float|null $price_net
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem whereComp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingOrderItem wherePriceNet($value)
|
||||
*
|
||||
* @property int|null $homeparty_id
|
||||
* @property-read \App\Models\Homeparty|null $homeparty
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem whereHomepartyId($value)
|
||||
*
|
||||
* @property string|null $tax
|
||||
* @property string|null $price_vk_net
|
||||
* @property string|null $discount
|
||||
* @property int|null $points
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem whereDiscount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem wherePoints($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem wherePriceVkNet($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem whereTax($value)
|
||||
*
|
||||
* @property int|null $shopping_collect_order_id
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrderItem whereShoppingCollectOrderId($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ShoppingOrderItem extends Model
|
||||
|
|
@ -64,6 +76,7 @@ class ShoppingOrderItem extends Model
|
|||
protected $table = 'shopping_order_items';
|
||||
|
||||
use SoftDeletes;
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $fillable = [
|
||||
|
|
@ -73,6 +86,7 @@ class ShoppingOrderItem extends Model
|
|||
'homeparty_id',
|
||||
'shopping_collect_order_id',
|
||||
'comp',
|
||||
'is_abo_addon',
|
||||
'qty',
|
||||
'price',
|
||||
'price_net',
|
||||
|
|
@ -83,6 +97,7 @@ class ShoppingOrderItem extends Model
|
|||
'points',
|
||||
'slug',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'qty' => 'int',
|
||||
'price' => 'float',
|
||||
|
|
@ -92,21 +107,38 @@ class ShoppingOrderItem extends Model
|
|||
'price_vk_net' => 'float',
|
||||
'discount' => 'float',
|
||||
'points' => 'float',
|
||||
'is_abo_addon' => 'bool',
|
||||
];
|
||||
|
||||
public function shopping_order()
|
||||
{
|
||||
return $this->belongsTo('App\Models\ShoppingOrder','shopping_order_id');
|
||||
return $this->belongsTo('App\Models\ShoppingOrder', 'shopping_order_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reine Abo-Positionen (keine einmalig hinzugefügten Artikel).
|
||||
*/
|
||||
public function scopeAboItems($query)
|
||||
{
|
||||
return $query->where('is_abo_addon', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Einmalig zum Abo-Versand hinzugefügte Artikel aus dem normalen Bestellsortiment.
|
||||
*/
|
||||
public function scopeAddonItems($query)
|
||||
{
|
||||
return $query->where('is_abo_addon', true);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Product','product_id');
|
||||
return $this->belongsTo('App\Models\Product', 'product_id');
|
||||
}
|
||||
|
||||
public function homeparty()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Homeparty','homeparty_id');
|
||||
return $this->belongsTo('App\Models\Homeparty', 'homeparty_id');
|
||||
}
|
||||
|
||||
public function getFormattedPrice()
|
||||
|
|
@ -139,7 +171,6 @@ class ShoppingOrderItem extends Model
|
|||
return formatNumber($this->attributes['tax']);
|
||||
}
|
||||
|
||||
|
||||
public function getFormattedDiscount()
|
||||
{
|
||||
return cleanNumberFormat($this->attributes['discount']);
|
||||
|
|
@ -147,7 +178,7 @@ class ShoppingOrderItem extends Model
|
|||
|
||||
public function getFormattedTotalPriceNet()
|
||||
{
|
||||
return formatNumber($this->attributes['price_net'] * $this->attributes['qty']);
|
||||
return formatNumber($this->attributes['price_net'] * $this->attributes['qty']);
|
||||
}
|
||||
|
||||
public function setPointsAttribute($value)
|
||||
|
|
@ -157,6 +188,6 @@ class ShoppingOrderItem extends Model
|
|||
|
||||
public function getFormattedPoints()
|
||||
{
|
||||
return isset($this->attributes['points']) ? formatNumber($this->attributes['points']) : "";
|
||||
return isset($this->attributes['points']) ? formatNumber($this->attributes['points']) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property-read User|null $member
|
||||
* @property-read \App\Models\ShoppingUser $shopping_user
|
||||
* @property-read Collection<int, \App\Models\UserAboItem> $user_abo_items
|
||||
* @property-read Collection<int, \App\Models\UserAboOneTimeItem> $one_time_items
|
||||
* @property-read int|null $user_abo_items_count
|
||||
* @property-read int|null $user_abo_orders_count
|
||||
*
|
||||
|
|
@ -173,6 +174,11 @@ class UserAbo extends Model
|
|||
return $this->hasMany(UserAboItem::class);
|
||||
}
|
||||
|
||||
public function one_time_items()
|
||||
{
|
||||
return $this->hasMany(UserAboOneTimeItem::class);
|
||||
}
|
||||
|
||||
public function user_abo_item_histories()
|
||||
{
|
||||
return $this->hasMany(UserAboItemHistory::class);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use Yard;
|
|||
* @property Carbon|null $updated_at
|
||||
* @property Product $product
|
||||
* @property UserAbo $user_abo
|
||||
* @package App\Models
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem query()
|
||||
|
|
@ -36,55 +36,57 @@ use Yard;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAboItem whereUserAboId($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserAboItem extends Model
|
||||
{
|
||||
protected $table = 'user_abo_items';
|
||||
protected $table = 'user_abo_items';
|
||||
|
||||
protected $casts = [
|
||||
'user_abo_id' => 'int',
|
||||
'product_id' => 'int',
|
||||
'comp' => 'int',
|
||||
'qty' => 'int',
|
||||
'status' => 'int'
|
||||
];
|
||||
protected $casts = [
|
||||
'user_abo_id' => 'int',
|
||||
'product_id' => 'int',
|
||||
'comp' => 'int',
|
||||
'qty' => 'int',
|
||||
'status' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'user_abo_id',
|
||||
'product_id',
|
||||
'comp',
|
||||
'qty',
|
||||
'status'
|
||||
];
|
||||
protected $fillable = [
|
||||
'user_abo_id',
|
||||
'product_id',
|
||||
'comp',
|
||||
'qty',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function user_abo()
|
||||
{
|
||||
return $this->belongsTo(UserAbo::class);
|
||||
}
|
||||
public function user_abo()
|
||||
{
|
||||
return $this->belongsTo(UserAbo::class);
|
||||
}
|
||||
|
||||
public function getPrice()
|
||||
{
|
||||
$ufactor = $this->user_abo->is_for === 'me' ? true : false;
|
||||
$tax_free = $ufactor ? true : Yard::instance('shopping')->getUserTaxFree();
|
||||
$userCountry = Yard::instance('shopping')->getUserCountry();
|
||||
return $this->product->getPriceWith($tax_free, $ufactor, $userCountry);
|
||||
}
|
||||
public function getPrice()
|
||||
{
|
||||
$ufactor = $this->user_abo->is_for === 'me' ? true : false;
|
||||
$tax_free = $ufactor ? true : Yard::instance(\App\Services\AboOrderCart::INSTANCE)->getUserTaxFree();
|
||||
$userCountry = Yard::instance(\App\Services\AboOrderCart::INSTANCE)->getUserCountry();
|
||||
|
||||
return $this->product->getPriceWith($tax_free, $ufactor, $userCountry);
|
||||
}
|
||||
|
||||
public function getFormattedPrice(){
|
||||
/** der Preis wird für den User berechnet */
|
||||
return Util::formatNumber($this->getPrice());
|
||||
}
|
||||
public function getFormattedPrice()
|
||||
{
|
||||
/** der Preis wird für den User berechnet */
|
||||
return Util::formatNumber($this->getPrice());
|
||||
}
|
||||
|
||||
|
||||
public function getFormattedTotalPrice(){
|
||||
/** der Preis wird für den User berechnet */
|
||||
return Util::formatNumber($this->getPrice() * $this->qty);
|
||||
}
|
||||
public function getFormattedTotalPrice()
|
||||
{
|
||||
/** der Preis wird für den User berechnet */
|
||||
return Util::formatNumber($this->getPrice() * $this->qty);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
app/Models/UserAboOneTimeItem.php
Normal file
101
app/Models/UserAboOneTimeItem.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\Util;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Class UserAboOneTimeItem
|
||||
*
|
||||
* Einmalig dem Abo hinzugefügte Produkte aus dem normalen Bestellsortiment.
|
||||
* Bewusst getrennt von {@see UserAboItem}, damit diese Positionen nie in die
|
||||
* dauerhafte Abo-Logik geraten und nach erfolgreicher Ausführung gelöscht werden.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_abo_id
|
||||
* @property int $product_id
|
||||
* @property int|null $comp
|
||||
* @property int $qty
|
||||
* @property int|null $confirmed_qty
|
||||
* @property \Illuminate\Support\Carbon|null $confirmed_at
|
||||
* @property float|null $price
|
||||
* @property float|null $price_net
|
||||
* @property float|null $tax_rate
|
||||
* @property float|null $tax
|
||||
* @property float|null $price_vk_net
|
||||
* @property float|null $discount
|
||||
* @property int|null $points
|
||||
* @property int $status
|
||||
* @property-read Product $product
|
||||
* @property-read UserAbo $user_abo
|
||||
*/
|
||||
class UserAboOneTimeItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'user_abo_one_time_items';
|
||||
|
||||
protected $casts = [
|
||||
'user_abo_id' => 'int',
|
||||
'product_id' => 'int',
|
||||
'comp' => 'int',
|
||||
'qty' => 'int',
|
||||
'confirmed_qty' => 'int',
|
||||
'confirmed_at' => 'datetime',
|
||||
'price' => 'float',
|
||||
'price_net' => 'float',
|
||||
'tax_rate' => 'float',
|
||||
'tax' => 'float',
|
||||
'price_vk_net' => 'float',
|
||||
'discount' => 'float',
|
||||
'points' => 'int',
|
||||
'status' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'user_abo_id',
|
||||
'product_id',
|
||||
'comp',
|
||||
'qty',
|
||||
'confirmed_qty',
|
||||
'confirmed_at',
|
||||
'price',
|
||||
'price_net',
|
||||
'tax_rate',
|
||||
'tax',
|
||||
'price_vk_net',
|
||||
'discount',
|
||||
'points',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function user_abo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserAbo::class);
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool
|
||||
{
|
||||
return $this->confirmed_at !== null && $this->confirmed_qty === $this->qty;
|
||||
}
|
||||
|
||||
public function getFormattedPrice(): string
|
||||
{
|
||||
return Util::formatNumber($this->price);
|
||||
}
|
||||
|
||||
public function getFormattedTotalPrice(): string
|
||||
{
|
||||
return Util::formatNumber($this->price * $this->qty);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,12 @@ class AboHelper
|
|||
*/
|
||||
public const MIN_DAYS_UNTIL_FIRST_ABO_EXECUTION = 10;
|
||||
|
||||
/**
|
||||
* Standard-Zeitfenster (Kalendertage) vor der Ausführung, in dem einmalige
|
||||
* Produkte aus dem normalen Sortiment hinzugefügt werden dürfen.
|
||||
*/
|
||||
public const DEFAULT_ONETIME_WINDOW_DAYS = 4;
|
||||
|
||||
public static $txaction_filter_text = [
|
||||
'paid' => 'paymend_paid',
|
||||
'appointed' => 'paymend_open',
|
||||
|
|
@ -168,6 +174,41 @@ class AboHelper
|
|||
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfiguriertes Zeitfenster (Kalendertage) für einmalige Produkte vor der Ausführung.
|
||||
*/
|
||||
public static function getOneTimeWindowDays(): int
|
||||
{
|
||||
$days = (int) \App\Models\Setting::getContentBySlug('abo-onetime-window-days');
|
||||
|
||||
return $days > 0 ? $days : self::DEFAULT_ONETIME_WINDOW_DAYS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob für dieses Abo aktuell einmalig Produkte aus dem normalen
|
||||
* Bestellsortiment hinzugefügt werden dürfen (Zeitfenster vor der Ausführung).
|
||||
*
|
||||
* Nur die zeitliche Bedingung wird geprüft. Aufrufer sollten zusätzlich den
|
||||
* Abo-Zustand (active/status) berücksichtigen, falls relevant.
|
||||
*/
|
||||
public static function isOneTimeWindowOpen(UserAbo $userAbo): bool
|
||||
{
|
||||
if (! $userAbo->next_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$today = Carbon::today();
|
||||
$nextDate = Carbon::parse($userAbo->next_date)->startOfDay();
|
||||
|
||||
if ($nextDate->lt($today)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$daysUntilExecution = $today->diffInDays($nextDate);
|
||||
|
||||
return $daysUntilExecution <= self::getOneTimeWindowDays();
|
||||
}
|
||||
|
||||
public static function getFirstAboDate($date, $abo_interval)
|
||||
{
|
||||
$reference = Carbon::parse($date)->startOfDay();
|
||||
|
|
|
|||
181
app/Services/AboOneTimeService.php
Normal file
181
app/Services/AboOneTimeService.php
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
|
||||
class AboOneTimeService
|
||||
{
|
||||
/**
|
||||
* Verarbeitet eine Einmal-Artikel-Aktion (add/update/remove) für ein Abo.
|
||||
*
|
||||
* Erwartet einen bereits initialisierten Yard (für die Snapshot-Berechnung beim
|
||||
* Hinzufügen). Gibt eine Fehlermeldung zurück oder null bei Erfolg.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function handleAction(UserAbo $userAbo, array $data): ?string
|
||||
{
|
||||
$action = $data['action'] ?? null;
|
||||
|
||||
return match ($action) {
|
||||
'add' => $this->add($userAbo, (int) ($data['product_id'] ?? 0)),
|
||||
'update' => $this->update($userAbo, (int) ($data['one_time_item_id'] ?? 0), (int) ($data['qty'] ?? 1)),
|
||||
'remove' => $this->remove($userAbo, (int) ($data['one_time_item_id'] ?? 0)),
|
||||
'confirm' => $this->confirm($userAbo),
|
||||
'discard' => $this->discard($userAbo),
|
||||
default => __('abo.onetime_action_invalid'),
|
||||
};
|
||||
}
|
||||
|
||||
private function add(UserAbo $userAbo, int $productId): ?string
|
||||
{
|
||||
$product = Product::find($productId);
|
||||
$allowedIds = ProductOrderContext::allowedShowOnIds(false, $userAbo->is_for === 'me' ? 'me' : 'ot-customer');
|
||||
|
||||
if (! $product || ! $product->active || ! ProductOrderContext::productMatchesShowOn($product, $allowedIds)) {
|
||||
return __('abo.onetime_product_not_allowed');
|
||||
}
|
||||
|
||||
if (AboOrderCart::exceedsMaxWeight($userAbo, (int) $product->weight)) {
|
||||
return __('msg.cart_max_weight_reached');
|
||||
}
|
||||
|
||||
$existing = UserAboOneTimeItem::withTrashed()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->where('product_id', $product->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->trashed()) {
|
||||
$existing->restore();
|
||||
$existing->qty = $existing->confirmed_at ? max(1, (int) $existing->confirmed_qty) + 1 : 1;
|
||||
} else {
|
||||
$existing->qty = min(100, $existing->qty + 1);
|
||||
}
|
||||
$existing->qty = min(100, $existing->qty);
|
||||
$existing->status = 0;
|
||||
$existing->save();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$snapshot = AboOrderCart::buildOneTimeSnapshot($product, $userAbo);
|
||||
UserAboOneTimeItem::create(array_merge($snapshot, [
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'product_id' => $product->id,
|
||||
'comp' => 0,
|
||||
'qty' => 1,
|
||||
'status' => 0,
|
||||
]));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function update(UserAbo $userAbo, int $itemId, int $qty): ?string
|
||||
{
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $userAbo->id)->find($itemId);
|
||||
if (! $item) {
|
||||
return __('abo.abo_item_not_found');
|
||||
}
|
||||
|
||||
$targetQty = max(1, min(100, $qty));
|
||||
$product = Product::find($item->product_id);
|
||||
if ($product) {
|
||||
$additionalWeight = (int) $product->weight * ($targetQty - (int) $item->qty);
|
||||
if ($additionalWeight > 0 && AboOrderCart::exceedsMaxWeight($userAbo, $additionalWeight)) {
|
||||
return __('msg.cart_max_weight_reached');
|
||||
}
|
||||
}
|
||||
|
||||
$item->qty = $targetQty;
|
||||
$item->status = $item->isConfirmed() ? 1 : 0;
|
||||
$item->save();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function remove(UserAbo $userAbo, int $itemId): ?string
|
||||
{
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $userAbo->id)->find($itemId);
|
||||
if (! $item) {
|
||||
return __('abo.abo_item_not_found');
|
||||
}
|
||||
|
||||
if ($item->confirmed_at) {
|
||||
$item->delete();
|
||||
} else {
|
||||
$item->forceDelete();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function confirm(UserAbo $userAbo): ?string
|
||||
{
|
||||
UserAboOneTimeItem::withTrashed()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->get()
|
||||
->each(function (UserAboOneTimeItem $item): void {
|
||||
if ($item->trashed()) {
|
||||
$item->forceDelete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$item->confirmed_qty = $item->qty;
|
||||
$item->confirmed_at = now();
|
||||
$item->status = 1;
|
||||
$item->save();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function discard(UserAbo $userAbo): ?string
|
||||
{
|
||||
UserAboOneTimeItem::withTrashed()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->get()
|
||||
->each(function (UserAboOneTimeItem $item): void {
|
||||
if (! $item->confirmed_at) {
|
||||
$item->forceDelete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($item->trashed()) {
|
||||
$item->restore();
|
||||
}
|
||||
|
||||
$item->qty = max(1, (int) $item->confirmed_qty);
|
||||
$item->status = 1;
|
||||
$item->save();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function hasUnconfirmedChanges(UserAbo $userAbo): bool
|
||||
{
|
||||
return UserAboOneTimeItem::withTrashed()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->get()
|
||||
->contains(function (UserAboOneTimeItem $item): bool {
|
||||
if ($item->trashed()) {
|
||||
return $item->confirmed_at !== null;
|
||||
}
|
||||
|
||||
return ! $item->isConfirmed();
|
||||
});
|
||||
}
|
||||
|
||||
public static function hasConfirmedItems(UserAbo $userAbo): bool
|
||||
{
|
||||
return UserAboOneTimeItem::where('user_abo_id', $userAbo->id)
|
||||
->whereNotNull('confirmed_at')
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,18 +5,66 @@ 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)
|
||||
{
|
||||
|
||||
|
|
@ -26,8 +74,11 @@ class AboOrderCart
|
|||
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('shopping');
|
||||
$yard = Yard::instance(self::INSTANCE);
|
||||
$itemsBeforeDestroy = $yard->content()->count();
|
||||
$yard->destroy();
|
||||
|
||||
|
|
@ -81,7 +132,7 @@ class AboOrderCart
|
|||
|
||||
// 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('shopping');
|
||||
$yard = Yard::instance(self::INSTANCE);
|
||||
$itemsBefore = $yard->content()->count();
|
||||
$yard->destroy();
|
||||
|
||||
|
|
@ -113,9 +164,9 @@ class AboOrderCart
|
|||
foreach ($abo_items as $abo_item) {
|
||||
self::addProductToCart($abo_item);
|
||||
}
|
||||
Yard::instance('shopping')->reCalculateShippingPrice();
|
||||
Yard::instance(self::INSTANCE)->reCalculateShippingPrice();
|
||||
|
||||
$user_abo->amount = Yard::instance('shopping')->totalWithShipping(2, '.', '') * 100;
|
||||
$user_abo->amount = Yard::instance(self::INSTANCE)->totalWithShipping(2, '.', '') * 100;
|
||||
$user_abo->save();
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +174,12 @@ class AboOrderCart
|
|||
{
|
||||
|
||||
$product = Product::find($item->product_id);
|
||||
$tax_free = Yard::instance('shopping')->getUserTaxFree();
|
||||
$user_country = Yard::instance('shopping')->getUserCountry();
|
||||
$tax_free = Yard::instance(self::INSTANCE)->getUserTaxFree();
|
||||
$user_country = Yard::instance(self::INSTANCE)->getUserCountry();
|
||||
|
||||
if ($product) {
|
||||
if ($item->comp) {
|
||||
$cartItem = Yard::instance('shopping')->add(
|
||||
$cartItem = Yard::instance(self::INSTANCE)->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
1,
|
||||
|
|
@ -149,7 +200,7 @@ class AboOrderCart
|
|||
return true;
|
||||
}
|
||||
if (self::$is_for === 'ot-customer' || self::$is_for === 'abo-ot-customer') {
|
||||
$cartItem = Yard::instance('shopping')
|
||||
$cartItem = Yard::instance(self::INSTANCE)
|
||||
->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
|
|
@ -160,7 +211,7 @@ class AboOrderCart
|
|||
['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('shopping')
|
||||
$cartItem = Yard::instance(self::INSTANCE)
|
||||
->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
|
|
@ -179,44 +230,195 @@ class AboOrderCart
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('shopping')->getNumComp();
|
||||
if ($needNumComp > 0) {
|
||||
$UserAboItems = UserAboItem::where('user_abo_id', $user_abo->id)->where('comp', '>', 0)->get();
|
||||
if (count($UserAboItems) === $needNumComp) {
|
||||
return true;
|
||||
$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;
|
||||
}
|
||||
|
||||
// need to add
|
||||
if (count($UserAboItems) < $needNumComp) {
|
||||
$product = Product::whereActive(true)->where('shipping_addon', true)->whereJsonContains('show_on', '12')->orderBy('pos', 'DESC')->first();
|
||||
for ($i = count($UserAboItems); $i <= $needNumComp; $i++) {
|
||||
$UserAboItem = UserAboItem::create([
|
||||
'user_abo_id' => $user_abo->id,
|
||||
'product_id' => $product->id,
|
||||
'comp' => $i + 1,
|
||||
'qty' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
AboItemHistoryService::logSystemCompAdded($user_abo, $UserAboItem);
|
||||
self::addProductToCart($UserAboItem);
|
||||
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();
|
||||
}
|
||||
}
|
||||
// need to remove
|
||||
if (count($UserAboItems) > $needNumComp) {
|
||||
foreach ($UserAboItems as $UserAboItem) {
|
||||
if ($UserAboItem->comp > $needNumComp) {
|
||||
AboItemHistoryService::logSystemCompRemoved($user_abo, $UserAboItem);
|
||||
$UserAboItem->delete();
|
||||
}
|
||||
}
|
||||
foreach (Yard::instance('shopping')->content() as $row) {
|
||||
if ($row->options->comp > $needNumComp) {
|
||||
Yard::instance('shopping')->remove($row->rowId);
|
||||
}
|
||||
|
||||
foreach (Yard::instance(self::INSTANCE)->content() as $row) {
|
||||
if (($row->options->comp ?? 0) > $needNumComp) {
|
||||
Yard::instance(self::INSTANCE)->remove($row->rowId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,34 @@ class Yard extends Cart
|
|||
$this->calculateShippingPrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Höchstes Versandgewicht (in Gramm) des aktuell gesetzten Versandlandes.
|
||||
* Ergibt sich aus der obersten Gewichtsstufe (max. weight_to). 0 = keine Begrenzung ermittelbar.
|
||||
*/
|
||||
public function getMaxWeight(): int
|
||||
{
|
||||
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
|
||||
if (! $shippingCountry || ! $shippingCountry->shipping) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $shippingCountry->shipping->shipping_prices->max('weight_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob das Warenkorbgewicht zzgl. des optionalen Zusatzgewichts das
|
||||
* Maximalgewicht des Versandlandes überschreiten würde.
|
||||
*/
|
||||
public function exceedsMaxWeight(int $additionalWeight = 0): bool
|
||||
{
|
||||
$maxWeight = $this->getMaxWeight();
|
||||
if ($maxWeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->weight() + $additionalWeight) > $maxWeight;
|
||||
}
|
||||
|
||||
public function setShippingCountryWithPrice($shipping_country_id, $shipping_is_for = 'ot-member')
|
||||
{
|
||||
$this->shipping_country_id = $shipping_country_id;
|
||||
|
|
@ -276,9 +304,10 @@ class Yard extends Cart
|
|||
// if(!$shipping_price){
|
||||
// }
|
||||
}
|
||||
// default
|
||||
// default: Über der höchsten Gewichtsstufe die teuerste (höchste) Stufe nehmen,
|
||||
// nicht die günstigste. Im Normalfall wird das Hinzufügen vorher gestoppt (siehe exceedsMaxWeight()).
|
||||
if (! $shipping_price) {
|
||||
$shipping_price = $shipping->shipping_prices->first();
|
||||
$shipping_price = $shipping->shipping_prices->sortByDesc('weight_to')->first();
|
||||
}
|
||||
}
|
||||
if ($shipping_price) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue