- Live-Review-Gate: Einmalprodukte nur fuer VIP im Sales Center sichtbar, Portal ausgeblendet (AboHelper::isOneTimeFeatureVisible + Gates in Controllern) - Nur verbindlich bestaetigte Einmal-Artikel fliessen in die Lieferung; Service-Helfer confirmedItems/pendingItems/pendingGross - Footer-Layout der Einmalprodukt-Liste: bestaetigte Summe + Gesamtbetrag, Trennstrich, offener Betrag und neue Gesamtsumme (dunkelgruen) - Uebersetzungen DE/EN/ES/FR (onetime_new_total u.a.), Tests angepasst/ergaenzt Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
6.7 KiB
PHP
215 lines
6.7 KiB
PHP
<?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;
|
|
}
|
|
|
|
/**
|
|
* Verbindlich bestätigte Einmal-Artikel (werden mit der nächsten Lieferung versandt).
|
|
*
|
|
* @return \Illuminate\Support\Collection<int, UserAboOneTimeItem>
|
|
*/
|
|
public static function confirmedItems(UserAbo $userAbo): \Illuminate\Support\Collection
|
|
{
|
|
return $userAbo->one_time_items()
|
|
->with('product')
|
|
->get()
|
|
->filter(fn (UserAboOneTimeItem $item): bool => $item->isConfirmed())
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* Noch nicht bestätigte Einmal-Artikel (Entwurf, nicht verbindlich).
|
|
*
|
|
* @return \Illuminate\Support\Collection<int, UserAboOneTimeItem>
|
|
*/
|
|
public static function pendingItems(UserAbo $userAbo): \Illuminate\Support\Collection
|
|
{
|
|
return $userAbo->one_time_items()
|
|
->with('product')
|
|
->get()
|
|
->reject(fn (UserAboOneTimeItem $item): bool => $item->isConfirmed())
|
|
->values();
|
|
}
|
|
|
|
public static function pendingGross(UserAbo $userAbo): float
|
|
{
|
|
return round(self::pendingItems($userAbo)
|
|
->sum(fn (UserAboOneTimeItem $item): float => (float) $item->price * (int) $item->qty), 2);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|