search) { $this->searchQuery( $orders, $request->search, function (Builder $q) use ($request) { $q->orWhereHas('transactions', function (Builder $q) use ($request) { $foreignIds = \collect(PaymentModeEnum::toArray()) ->map(function (PaymentModeEnum $paymentMode) use ($request) { return Transaction::getPaymentForeignKeyName( $paymentMode, $request->search ); }); $q->whereIn('foreign_id', $foreignIds) ->orWhereIn('foreign_refund_id', $foreignIds); }); }, 'reference', 'email', ); }//end if $this->sortQuery($orders); $searchFields = trans('Réference,Email,Pay.Ref'); if ($this->indexModeTodayOnly) { $orders->whereHas('timeslot', function (Builder $q) { $q->whereDate('slot', \now()->today()); }); } $orders->orderBy('created_at', 'DESC'); $orders->with(['deliveryMode', 'trashedTimeSlot.shop']); $orders = $orders->paginate(10); return view('back::pages.orders.index', compact('orders', 'searchFields')); } /** * Display a listing of the resource. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory */ public function indexToday(Request $request): View { $this->indexModeTodayOnly = true; /** @var \Illuminate\View\View $response */ $response = $this->callAction('index', ['request' => $request]); return $response; } /** * Display the specified resource. * * @param \App\Models\Shop\Order $order * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory */ public function show(Order $order): View { $order->load('transactions'); return view('back::pages.orders.show', compact('order')); } /** * Mettre a jour le status de validation d'une commande * * @param \App\Http\Requests\Bo\Orders\ChangeOrderValidationStatusRequest $request * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function changeValidationStatus(ChangeOrderValidationStatusRequest $request, Order $order): RedirectResponse { $this->authorize('changeValidationStatus', $order); return DB::transaction(function () use ($request, $order) { $order->validation_status = $request->validation_status; switch ($request->validation_status) { case ValidationStatus::cancelled()->value: \session()->flash('bo.warning', trans('La commande est marquée comme annulée')); // Fall through. case ValidationStatus::validated()->value: $order->saveOrFail(); return \response()->redirectToRoute('bo.orders.show', $order)->with('bo.success', trans( ':model a été mis à jour', ['model' => Str::singular(trans('models.classes.orders'))] )); case ValidationStatus::pending()->value: return \back()->with('bo.errors', \trans('Il faut choisir un statut pour cette commande.')); default: throw new \RuntimeException("Unhandled status `{$request->validation_status}`"); }//end switch }); } /** * Mettre a jour le status de livraison d'une commande * * @param \App\Http\Requests\Bo\Orders\ChangeOrderDeliveryStatusRequest $request * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function changeDeliveryStatus(ChangeOrderDeliveryStatusRequest $request, Order $order): RedirectResponse { $this->authorize('changeDeliveryStatus', $order); return DB::transaction(function () use ($request, $order) { $order->delivery_status = $request->delivery_status; switch (true) { // * La commande va être préparé ou est déja en cours de préparation case $order->delivery_status->equals(DeliveryStatus::preparation()) or $order->delivery_status->equals(DeliveryStatus::preparation()): $order->delivery_status = DeliveryStatus::preparation(); $order->shipped_basket = ''; $order->saveOrFail(); return \redirect()->route('bo.orders.prepare', $order); // * La commande est récupérée. case $order->delivery_status->equals(DeliveryStatus::recovered()): // * Déclarer la commande comme réglée si récupération à la boutique. if ($order->deliveryMode->mode->equals(DeliveryModeEnum::inShop())) { // ! There should be one and only one inShop transaction ! /** @var \App\Models\Payment\Transaction|null */ $inShopTransaction = $order->transactions() ->where('type', TransactionType::debit()) ->where('payment_mode', PaymentModeEnum::inShop()) ->latest()->first(); $paidTransactions = $order->transactions()->where('type', TransactionType::debit()) ->where('status', TransactionStatus::validated())->count(); // * Check if there are not paid transactions then // * we should have one pending inShop transaction. if (!$paidTransactions and !$inShopTransaction) { throw new \RuntimeException( "There should be one inshop transaction for order '{$order->id}'" ); } if ($inShopTransaction) { $inShopTransaction->status = TransactionStatus::validated(); $inShopTransaction->saveOrFail(); } } else { throw new \RuntimeException("Unhandled delivery mode other than in shop"); } //end if // Fall through. // * La commande est prête. case $order->delivery_status->equals(DeliveryStatus::ready()): $order->saveOrFail(); return \back()->with('bo.success', trans( ':model a été mis à jour', ['model' => Str::singular(trans('models.classes.orders'))] )); default: throw new \RuntimeException("Unhandled status `{$request->delivery_status}`"); }//end switch }); } /** * Show the order resolve form * * @param \App\Models\Shop\Order $order * @return \Illuminate\View\View * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass */ public function resolve(Order $order): View { // phpcs:enable $this->authorize('resolve', $order); $transactionModel = $order->transactions()->where('needs_validation', true)->first(); // * Diff between Basket - (Delivered items - refunded transaction items) $remainingItems = $order->getRemainingUnrefundedItems(); return \view('back::pages.orders.resolve', \compact('order', 'transactionModel', 'remainingItems')); } /** * Validate order resolution * * @param \App\Http\Requests\Bo\ResolveOrderRequest $request * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function resolveSubmit(ResolveOrderRequest $request, Order $order): RedirectResponse { return DB::transaction(function () use ($request, $order) { /** @var \App\Models\Payment\Transaction */ $transaction = $request->transaction; /** @var \Illuminate\Support\Collection */ $orderedItems = \collect($order->basket); // * getRemainingUnrefundedItems Modifie les rowId originaux, il faut les restaurer pour la comparaison. /** @var \Illuminate\Support\Collection */ $refundedItems = $request->collect('items')->mapWithKeys(function (array $item) use ($orderedItems) { $item = (object)$item; $rowId = $orderedItems->where('id', '=', $item->index)->first()->rowId; return [$rowId => $item->quantity]; }); /** @var \Illuminate\Support\Collection */ $refundedTransactionsItems = $orderedItems->map(function (object $basketItem) use ($refundedItems) { if ($refundedItems->has($basketItem->rowId)) { $basketItem->qty = \intval($refundedItems->get($basketItem->rowId)); return $basketItem; } return null; }); $refundedTransactionsItems = \collect(ToolboxHelper::updateBasketAmounts( $orderedItems, $refundedItems->all() )); $montantRembourse = \intval($refundedTransactionsItems->sum('options.price_ttc')); // ? La transaction concerne des produits ? if ($refundedTransactionsItems->count()) { // * Estampiller les ligne avec la raison $refundedTransactionsItems->map(function (object $basketItem) use ($request) { $basketItem->name = "{$request->reason} - {$basketItem->name}"; return $basketItem; }); } // ? Le montant des produits remboursé est inférieur a celui de la transaction. if ($montantRembourse < \intval($transaction->amount->getAmount())) { // * Créer un dummy product pour représenter la transaction. $cartItemOptions = \array_merge($this->cartItemOptionsSkel, [ 'virtual' => true, 'price_ttc' => \intval($transaction->amount->getAmount()) ]); $cartItem = CartItem::fromArray([ 'id' => \str_replace('.', '', \uniqid('', true)), 'name' => $request->reason, 'price' => \intval($transaction->amount->getAmount()) - $montantRembourse, 'weight' => 0, 'options' => $cartItemOptions ]); $cartItem->setQuantity(1); // * On envoie la ligne virtuelle en haut de la liste. $refundedTransactionsItems->put($cartItem->rowId, (object)$cartItem->toArray()); $refundedTransactionsItems = $refundedTransactionsItems->reverse(); } $transaction->items = $refundedTransactionsItems; $transaction->needs_validation = false; $transaction->saveOrFail(); return \response()->redirectToRoute('bo.orders.show', $order) ->with('success', \trans('La transaction est marquée comme résolue')); }); } /** * Show the order prepare form * * @param \App\Models\Shop\Order $order * @return \Illuminate\View\View * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass */ public function prepare(Order $order): View { // phpcs:enable $this->authorize('prepare', $order); $remainingItems = $order->getRemainingUnrefundedItems(); return \view('back::pages.orders.prepare', \compact('order', 'remainingItems')); } /** * Validate order preparation * * @param \App\Http\Requests\Bo\PrepareOrderRequest $request * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function prepareSubmit(PrepareOrderRequest $request, Order $order): RedirectResponse { $this->authorize('prepare', $order); return DB::transaction(function () use ($request, $order) { $remainingItems = $order->getRemainingUnrefundedItems(); $shipped_basket = \collect(ToolboxHelper::updateBasketAmounts( $remainingItems, $request->items )); $itemsToRefund = $remainingItems->pluck('qty', 'id') ->mapWithKeys(function ($qty, string $id) use ($request) { return [ $id => \intval($qty) - \intval(\collect($request->items)->get($id, 0)) ]; }); $refundedTransactionsItems = \collect(ToolboxHelper::updateBasketAmounts( $remainingItems, $itemsToRefund->all() )); // * Mettre a jour la commande. $order->shipped_basket = $shipped_basket; $order->delivery_status = DeliveryStatus::ready(); $order->saveOrFail(); // ? Le montant va exceder ce que le client a payé ? if ($request->willExceedOrderAmount) { // * Annulation et remboursement du reste. // * Créer un dummy product pour représenter la transaction. $cartItemOptions = \array_merge($this->cartItemOptionsSkel, [ 'virtual' => true, 'price_ttc' => $order->total_ttc - $order->total_refund, ]); $cartItem = CartItem::fromArray([ 'id' => \str_replace('.', '', \uniqid('', true)), 'name' => trans('Remboursement commande'), 'price' => $order->total_ttc - $order->total_refund, 'weight' => 0, 'options' => $cartItemOptions ]); $cartItem->setQuantity(1); $this->refundTransaction( $order, $order->total_ttc - $order->total_refund, \collect([ $cartItem->rowId => (object)$cartItem->toArray() ]) ); // * Cancel Order completely. /** @var \Illuminate\Http\RedirectResponse */ return $this->callAction('changeValidationStatus', [ ChangeOrderValidationStatusRequest::create( \route('bo.orders.validation-status', $order), 'POST', [ 'validation_status' => ValidationStatus::cancelled()->value ] ), $order ]); } //end if // ? La commande va devoir être remboursé completement ? if ($request->willNeedCompleteRefund) { $this->refundTransaction( $order, $order->total_ttc - $order->total_refund, $order->getRemainingUnrefundedItems() ); // * Cancel Order completely. /** @var \Illuminate\Http\RedirectResponse */ return $this->callAction('changeValidationStatus', [ ChangeOrderValidationStatusRequest::create( \route('bo.orders.validation-status', $order), 'POST', [ 'validation_status' => ValidationStatus::cancelled()->value ] ), $order ]); } //end if // ? Il faudra emmtre un remboursment pour le reste ? if ($request->willNeedRefund) { $this->refundTransaction( $order, $shipped_basket->sum('subtotal'), $refundedTransactionsItems ); } return \redirect()->route('bo.orders.show', $order)->with('bo.success', trans( ':model a été mis à jour', ['model' => Str::singular(trans('models.classes.orders'))] )); }); } /** * Complete order refund and cancel * * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function cancelRefundSubmit(Order $order): RedirectResponse { $this->authorize('refund', $order); return DB::transaction(function () use ($order) { $this->refundTransaction( $order, $order->total_ttc - $order->total_refund, $order->getRemainingUnrefundedItems() ); // * Cancel Order completely. /** @var \Illuminate\Http\RedirectResponse */ return $this->callAction('changeValidationStatus', [ ChangeOrderValidationStatusRequest::create( \route('bo.orders.validation-status', $order), 'POST', [ 'validation_status' => ValidationStatus::cancelled()->value ] ), $order ]); }); } /** * Complete order refund * * @param \App\Models\Shop\Order $order * @return \Illuminate\Http\RedirectResponse */ public function refundSubmit(Order $order): RedirectResponse { $this->authorize('refund', $order); return DB::transaction(function () use ($order) { $this->refundTransaction( $order, $order->total_ttc - $order->total_refund, $order->getRemainingUnrefundedItems() ); return \back()->with('bo.success', trans( ':model a été mis à jour', ['model' => Str::singular(trans('models.classes.orders'))] )); }); } /** * Refund a transaction * * @param \App\Models\Shop\Order $order * @param integer $amount * @param object $items * @return void * @throws \RuntimeException If the payment transaction cannot be fetched for the concerned order. */ private function refundTransaction( Order $order, int $amount, object $items ): void { if (!($transaction = $order->getTransactionUsedForPayment())) { throw new \RuntimeException("Could not fetch payment transaction for order {$order->id}"); } // ? Le paiement se fait en boutique ? if ($transaction->payment_mode->equals(PaymentModeEnum::inShop())) { // ? La validation est annulée et le montant a remboursé equivaut a montant de la commande ? if ( $order->validation_status->equals(ValidationStatus::cancelled()) and $order->total_ttc === $amount ) { // * Mettre la transaction inShop comme ok et la commande comme remboursée DB::transaction(function () use ($order, $transaction, $amount) { $order->total_refund = $amount; $order->payment_status = PaymentStatus::refunded(); $order->saveQuietly(); $transaction->status = TransactionStatus::validated(); $transaction->saveQuietly(); $previousMessage = session()->get('bo.warning', ''); session()->flash('bo.warning', \trans( 'La commande est marquée comme remboursée' ) . " {$previousMessage}"); }); return; } $previousMessage = session()->get('bo.error', ''); session()->flash( 'bo.error', \trans( 'Vous devrez rembourser vous-même le client au magasin' ) . " {$previousMessage}" ); return; } //end if RefundOrder::dispatch( $order, $transaction, $amount, $items )->onConnection('database'); $previousMessage = session()->get('bo.info', ''); session()->flash('bo.info', \trans( 'Un remboursement va être émit au client' ) . " \n{$previousMessage}"); } }