code/app/Services/BookingsService.php
<?php
namespace App\Services;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use App\Exceptions\AuthException;
use App\Exceptions\IllegalArgumentException;
use App\Services\Concerns\TranslatesBookings;
use App\BookedProductVariant;
use App\BookedProductComponent;
use App\ModifierType;
use App\ModifiedValue;
use App\Events\BookingDelivered;
class BookingsService extends BaseService
{
use TranslatesBookings;
protected function testAccess($target, $orders, $delivering)
{
$user = Auth::user();
$valid = false;
/*
È sufficiente che l'utente abbia accesso in consegna anche ad un
solo fornitore per ottenere accesso all'intero aggregato; questo
serve a non avere grattacapi in caso di ordini aggregati i cui
permessi non sono accurati
*/
foreach($orders as $order) {
if ($user->can('supplier.shippings', $order->supplier)) {
$valid = true;
break;
}
if ($delivering == false) {
if ($target->testUserAccess($user)) {
$valid = true;
break;
}
}
}
if ($valid == false) {
throw new AuthException(403);
}
return $user;
}
private function initVariant($booked, $quantity, $delivering, $values)
{
if ($quantity == 0) {
return null;
}
$bpv = new BookedProductVariant();
$bpv->product_id = $booked->id;
if ($delivering == false) {
$bpv->quantity = $quantity;
$bpv->delivered = 0;
}
else {
$bpv->quantity = 0;
$bpv->delivered = $quantity;
}
$bpv->save();
$components = [];
foreach ($values as $variant_id => $value_id) {
$bpc = new BookedProductComponent();
$bpc->productvariant_id = $bpv->id;
$bpc->variant_id = $variant_id;
$bpc->value_id = $value_id;
$components[] = $bpc;
}
$bpv->components()->saveMany($components);
return $bpv;
}
private function findVariant($booked, $values, $saved_variants)
{
$query = BookedProductVariant::where('product_id', $booked->id);
foreach ($values as $variant_id => $value_id) {
$query->whereHas('components', function ($q) use ($variant_id, $value_id) {
$q->where('variant_id', $variant_id)->where('value_id', $value_id);
});
}
return $query->whereNotIn('id', $saved_variants)->first();
}
private function adjustVariantValues($values, $i)
{
$real_values = [];
foreach($values as $variant_id => $vals) {
if (isset($vals[$i]) && !empty($vals[$i])) {
$real_values[$variant_id] = $vals[$i];
}
}
return $real_values;
}
private function handlingParam($delivering) {
if ($delivering == false) {
return 'quantity';
}
else {
return 'delivered';
}
}
private function readVariants($product, $booked, $values, $quantities, $delivering)
{
$param = $this->handlingParam($delivering);
$quantity = 0;
$saved_variants = [];
$param = $this->handlingParam($delivering);
for ($i = 0; $i < count($quantities); ++$i) {
$q = (float) $quantities[$i];
$real_values = $this->adjustVariantValues($values, $i);
if (empty($real_values)) {
continue;
}
$bpv = $this->findVariant($booked, $real_values, $saved_variants);
if (is_null($bpv)) {
$bpv = $this->initVariant($booked, $q, $delivering, $real_values);
if (is_null($bpv)) {
continue;
}
}
else {
if ($q == 0 && $delivering == false) {
$bpv->delete();
continue;
}
if ($bpv->$param != $q) {
$bpv->$param = $q;
$bpv->save();
}
}
$saved_variants[] = $bpv->id;
$quantity += $q;
}
/*
Attenzione: in fase di consegna/salvataggio è lecito che una
quantità sia a zero, ma ciò non implica eliminare la variante
*/
if ($delivering == false) {
BookedProductVariant::where('product_id', '=', $booked->id)->whereNotIn('id', $saved_variants)->delete();
}
/*
Per ogni evenienza qui ricarico le varianti appena salvate, affinché
il computo del prezzo totale finale per il prodotto risulti corretto
*/
$booked->unsetRelation('variants');
return [$booked, $quantity];
}
/*
TODO: il processo di lettura della prenotazione dalla $request andrebbe
spostato altrove, in una struttura dati dedicata, da usare anche in
altre circostanze (e.g. l'importazione da CVS, che attualmente
ricostruisce una Request farlocca ed inutilmente complessa).
All'occorrenza lì potrebbe finirci anche la procedura di validazione
delle quantità secondo i constraints attivi
*/
private function readBooking(array $request, $order, $booking, $delivering)
{
$param = $this->handlingParam($delivering);
if (isset($request['notes_' . $order->id])) {
$booking->notes = $request['notes_' . $order->id] ?? '';
}
$existed_before = $booking->exists;
$booking->save();
$count_products = 0;
$booked_products = new Collection();
/*
In caso di ordini chiusi ma con confezioni da completare, ci
sono un paio di casi speciali...
O sto prenotando tra i prodotti da completare, e dunque devo
intervenire solo su di essi (nel form booking.edit viene
aggiunto un campo nascosto "limited") senza intaccare le
quantità già prenotate degli altri, oppure sono un
amministratore e sto intervenendo sull'intera prenotazione
(dunque posso potenzialmente modificare tutto).
*/
if (isset($request['limited'])) {
$products = $order->status == 'open' ? $order->products : $order->pendingPackages();
}
else {
$products = $order->products;
}
foreach ($products as $product) {
/*
$booking->getBooked() all'occorrenza crea un nuovo
BookedProduct, che deve essere salvato per potergli agganciare
le varianti.
Ma se la quantità è 0 (e bisogna badare che lo sia in caso di
varianti che senza varianti) devo evitare di salvare tale
oggetto temporaneo, che andrebbe solo a complicare le cose nel
database
*/
$quantity = (float) ($request[$product->id] ?? 0);
if (empty($quantity)) {
$quantity = 0;
}
$quantities = [];
if ($product->variants->isEmpty() == false) {
$quantities = $request['variant_quantity_' . $product->id] ?? '';
if (empty($quantities)) {
continue;
}
}
$booked = $booking->getBooked($product, true);
if ($quantity != 0 || !empty($quantities)) {
$booked->save();
if ($product->variants->isEmpty() == false) {
$values = [];
foreach ($product->variants as $variant) {
if (isset($request['variant_selection_' . $variant->id])) {
$values[$variant->id] = $request['variant_selection_' . $variant->id];
}
}
list($booked, $quantity) = $this->readVariants($product, $booked, $values, $quantities, $delivering);
}
}
if ($delivering == false && $quantity == 0) {
$booked->delete();
}
else {
if ($booked->$param != 0 || $quantity != 0) {
$booked->$param = $quantity;
$booked->save();
$count_products++;
$booked_products->push($booked);
}
}
}
/*
Attenzione: se sto consegnando, e tutte le quantità sono a 0,
comunque devo preservare i dati della prenotazione (se esistono).
Va anche contemplato il caso in cui sto consegnando un ordine
aggregato e l'utente non ha partecipato a qualcuno degli ordini; in
tal caso, la sua prenotazione vuota non va salvata
*/
if (($delivering == false || $existed_before == false) && $booked_products->count() == 0) {
$booking->delete();
return null;
}
else {
$booking->setRelation('products', $booked_products);
return $booking;
}
}
private function deliveringManualTotal($request, $order)
{
if (isset($request['manual_total_' . $order->id])) {
$manual_total = $request['manual_total_' . $order->id];
if (filled($manual_total)) {
return $manual_total;
}
}
return 0;
}
protected function handlePreProcess($request, $booking)
{
$manual_total = $this->deliveringManualTotal($request, $booking->order);
if ($manual_total > 0) {
$booking->enforceTotal($manual_total);
}
foreach(array_keys($request) as $key) {
/*
Qui faccio il controllo sui prezzi applicati solo se
effettivamente se ne trovano nella richiesta, altrimenti finisco
col tirare su tutte le varianti combo presenti nell'ordine
*/
if (str_starts_with($key, 'apply_price_')) {
foreach($booking->order->products as $prod) {
$key = sprintf('apply_price_%s', $prod->id);
if (isset($request[$key])) {
$prod->setPrice($request[$key]);
}
$combos = $prod->variant_combos;
foreach($combos as $combo) {
$key = sprintf('apply_price_%s_%s', $prod->id, $combo->id);
if (isset($request[$key])) {
$combo->setPrice($request[$key]);
}
}
}
break;
}
}
return $booking;
}
protected function handlePostProcess($request, $booking)
{
$manual_total = $this->deliveringManualTotal($request, $booking->order);
if ($manual_total > 0) {
$manual_adjust_modifier = ModifierType::find('arrotondamento-consegna');
$modifier = $booking->order->modifiers()->where('modifier_type_id', $manual_adjust_modifier->id)->first();
if (is_null($modifier)) {
$modifier = $booking->order->attachEmptyModifier($manual_adjust_modifier);
}
$booking->modifiedValues()->whereHas('modifier', function($query) use ($manual_adjust_modifier) {
$query->where('modifier_type_id', $manual_adjust_modifier->id);
})->delete();
$modifier_value = new ModifiedValue();
$modifier_value->modifier_id = $modifier->id;
$modifier_value->target_type = get_class($booking);
$modifier_value->target_id = $booking->id;
$modifier_value->amount = $manual_total - $booking->getValue('effective', false, true);
$modifier_value->save();
$booking->unsetRelation('modifiedValues');
}
}
public function handleBookingUpdate($request, $user, $order, $target_user, $delivering)
{
/*
- recupero la prenotazione
- resetto lo stato dei pagamenti e dei modificatori. Questo per evitare di dover gestire gli aggiornamenti: ricalcolo daccapo tutto
- aggiorno i contenuti della prenotazione
- ricalcolo i modificatori
- gestisco i modificatori esterni (gli arrotondamenti sulle consegne manuali)
*/
$booking = $order->userBooking($target_user);
/*
Nel caso di un ordine aggregato in cui alcuni ordini sono aperti
e altri chiusi, nel pannello di prenotazione quelli chiusi non
sono editabili e pertanto non viene qui spedita nessuna
quantità. Dunque in fase di lettura si finirebbe col salvare
tutte le quantità a 0, con conseguente cancellazione di una
prenotazione comunque valida.
L'array skip_order viene sempre incluso nel template di sola
lettura della prenotazione, e serve ad identificare quelli da
saltare
*/
$skip = $request['skip_order'] ?? [];
if (in_array($order->id, $skip)) {
return $booking;
}
$booking->wipeStatus();
$booking = $this->handlePreProcess($request, $booking);
$booking = $this->readBooking($request, $order, $booking, $delivering);
if ($booking && $delivering) {
BookingDelivered::dispatch($booking, $request['action'], $user);
$this->handlePostProcess($request, $booking);
}
return $booking;
}
/*
Il controllo sul credito a disposizione viene fatto soprattutto
client-side, in fase di prenotazione; qui rifaccio il controllo per
prevenire eventuali problemi
*/
private function checkAvailableCredit($user)
{
if ($user->gas->hasFeature('restrict_booking_to_credit')) {
/*
Questa funzione viene invocata dopo aver salvato la
prenotazione, nel contesto di una transazione, dunque il
bilancio attivo dell'utente già prevede la prenotazione stessa e
pertanto, per essere valido, deve essere superiore al limite
*/
$current_active_balance = $user->activeBalance();
if ($current_active_balance < $user->gas->restrict_booking_to_credit['limit']) {
DB::rollback();
throw new IllegalArgumentException(_i('Credito non sufficiente'), 1);
}
}
}
public function bookingUpdate(array $request, $aggregate, $target_user, $delivering)
{
DB::beginTransaction();
$orders = $aggregate->orders()->with(['products', 'bookings', 'modifiers'])->get();
$user = $this->testAccess($target_user, $orders, $delivering);
foreach($orders as $order) {
$booking = $this->handleBookingUpdate($request, $user, $order, $target_user, $delivering);
if ($booking) {
$this->translateBooking($booking, $delivering, false);
}
}
if ($delivering == false) {
$this->checkAvailableCredit($target_user);
}
DB::commit();
}
}