false, 'shipped_basket' => '{}', 'total_refund' => 0 ]; /** * Map attribute names to enum classes. * * @var array */ protected $enumCasts = [ 'validation_status' => ValidationStatus::class, 'payment_status' => PaymentStatus::class, 'delivery_status' => DeliveryStatus::class ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'basket' => 'object', 'shipped_basket' => 'object', 'shipping' => 'object', 'billing' => 'object', 'mail_sent' => 'bool' ]; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'shipping', 'billing' ]; /** * Perform any actions required after the model boots. * * @return void */ protected static function booted() { static::creating(function (self $model) { // * ASSERTIONS static::assertValidateStatus($model); static::assertCancelStatus($model); $model->setUniqueOrderReference(); }); static::updating(function (self $model) { static::assertValidateStatus($model); static::assertCancelStatus($model); static::assertDeliveryStatusCanBeChanged($model); if ($model->validation_status->equals(ValidationStatus::validated())) { OrderIsValidated::dispatchUnless($model->mail_sent, $model); } /** @var \App\Models\Customer\Customer|null */ $customer = $model->customer; if ($model->validation_status->value !== $model->getOriginal('validation_status')) { // * Notifiy customer $notification = new OrderValidationStatusChanged($model); if ($customer) { $model->customer->notify($notification); } else { // * AnonymousNotifiable with Notification Fake Notification::route('mail', $model->email)->notifyNow($notification); } Session::flash('bo.info', trans('Le client recevra une notification sur l\'état de validation.')); // * Commande est prête. if ($model->delivery_status->equals(DeliveryStatus::ready())) { // * Notifiy customer $notification = new OrderDeliveryStatusChanged($model); if ($customer) { $model->customer->notify($notification); } else { // * AnonymousNotifiable with Notification Fake Notification::route('mail', $model->email)->notifyNow($notification); } Session::flash('bo.info', trans('Le client recevra une notification sur l\'état de livraison.')); } } //end if // * Commande est annulée if ( $model->validation_status->value !== $model->getOriginal('validation_status') and $model->validation_status->equals(ValidationStatus::cancelled()) ) { // * Set le timeslot as soft deleted $model->timeslot()->delete(); } // * Empecher la réactivation d'une commande (car le timeslot aura été perdu). if ( $model->getOriginal('validation_status') === ValidationStatus::cancelled()->value and !$model->validation_status->equals(ValidationStatus::cancelled()) ) { throw new \Error('This is not allowed'); } }); static::deleting(function () { throw new \Error('This is not allowed'); }); } /** * Get the route key for the model. * * @return string */ public function getRouteKeyName() { return 'reference'; } // * RELATIONSHIPS /** * HasMany transactions relation * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function transactions() { return $this->hasMany(Transaction::class); } /** * BelongsToMany products relation * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function products() { return $this->belongsToMany(Product::class, 'order_lines', 'order_id', 'product_id'); } /** * BelongsTo customer relation * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function customer() { return $this->belongsTo(Customer::class); } /** * BelongsTo DeliveryMode relation * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function deliveryMode() { return $this->belongsTo(DeliveryMode::class); } /** * hasOne timeslot relation * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function timeslot() { return $this->hasOne(Timeslot::class); } /** * hasOne timeslot relation * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function trashedTimeslot() { return $this->timeslot()->withTrashed(); } // * GETTER /** * Getter can_be_cancelled * * ! Logique modifiée pour refonte only here (sans paiement) * * On peut annuler la commande si : * - la livraison n'a pas encore été initié * - la commande n'est pas déjà annulée * * @return boolean */ public function getCanBeCancelledAttribute(): bool { // * Si le status de validation n'est pas annulé return $this->getOriginal('validation_status') !== ValidationStatus::cancelled()->value && // * Si le statut de livraison n'est pas récupéré $this->getOriginal('delivery_status') !== DeliveryStatus::recovered()->value; } /** * Getter can_be_refund * * * On peut rembourser la commande si le montant déjà remboursé est inférieur au montant payé * * @return boolean */ public function getCanBeRefundAttribute(): bool { return $this->total_ttc > $this->total_refund; } // * SETTER /** * Getter reference * * @return void * @throws \RuntimeException This is readonly. */ public function setReferenceAttribute(): void { throw new \RuntimeException('Reference is readonly'); } // * METHODS /** * Get the transaction used for payment * * @return null|\App\Models\Payment\Transaction */ public function getTransactionUsedForPayment(): ?Transaction { /** @var \App\Models\Payment\Transaction|null $transaction */ $transaction = $this->transactions()->where('type', TransactionType::debit()) ->where('status', TransactionStatus::validated())->first(); // If no paid transaction there is, try to fetch an inshop transaction. if (!$transaction and $this->deliveryMode->mode->equals(DeliveryModeEnum::inShop())) { $transaction = $this->transactions()->where('type', TransactionType::debit()) ->where('payment_mode', PaymentModeEnum::inShop()) ->where('status', TransactionStatus::pending()) ->first(); } return $transaction; } /** * Total payed by the customer * * @return \Money\Money */ public function getTotalPayed(): Money { return $this->transactions->reduce(function ($previous, Transaction $actual) { /** @var \Money\Money|null $previous */ /** @var \App\Models\Payment\Transaction $actual */ $out = new Money(0, app('phpMoneyCurrency')); if ($previous) { $out = $out->add($previous); } if ($actual->type->equals(TransactionType::debit())) { $out = $out->add($actual->amount); } return $out; }) ?? new Money(0, app('phpMoneyCurrency')); } /** * Total refunded to the customer * * @return \Money\Money */ public function getTotalRefunded(): Money { return $this->transactions->reduce(function ($previous, Transaction $actual) { /** @var \Money\Money|null $previous */ /** @var \App\Models\Payment\Transaction $actual */ $out = new Money(0, app('phpMoneyCurrency')); if ($previous) { $out = $out->add($previous); } if ($actual->type->equals(TransactionType::credit())) { $out = $out->add($actual->amount); } return $out; }) ?? new Money(0, app('phpMoneyCurrency')); } /** * Setup reference * * @return void */ public function setUniqueOrderReference() { if (!$this->reference) { $this->setRawAttributes(\array_merge( $this->attributes, [ 'reference' => ToolboxHelper::getUniqueReference(self::class) ] )); } } /** * Check if order can be cancelled * * @param self $model * @return void * @throws \Illuminate\Validation\ValidationException If the order is canceled while it cannot be. */ public static function assertCancelStatus(self $model) { if ( !$model->can_be_cancelled and $model->validation_status->equals(ValidationStatus::cancelled()) ) { throw ValidationException::withMessages([ 'messages' => "La commande '{$model->id}' ne peut être annulée" ]); } if ( $model->validation_status->equals(ValidationStatus::cancelled()) and $model->getTransactionUsedForPayment()->payment_mode->equals(PaymentModeEnum::inShop()) and !\in_array($model->payment_status, [PaymentStatus::failed(), PaymentStatus::pending()]) ) { throw ValidationException::withMessages([ 'messages' => "La commande '{$model->id}' ne peut être annulée car le remboursement click and collect n'est pas géré" ]); } } /** * Check if order can be validated * * @param self $model * @return void * @throws \Illuminate\Validation\ValidationException If the order is canceled while it cannot be. */ public static function assertValidateStatus(self $model) { if ($model->getOriginal('validation_status') === ValidationStatus::cancelled()->value) { throw ValidationException::withMessages([ 'messages' => "La commande '{$model->id}' ne peut être validée après avoir été annulée" ]); } } /** * Check if delivery status can be changed. * * @param self $model * @return void * @throws \Illuminate\Validation\ValidationException If the order is canceled while it cannot be. */ public static function assertDeliveryStatusCanBeChanged(self $model) { $deliveryStatusHasChanged = $model->getOriginal('delivery_status') !== $model->delivery_status->value; if ($deliveryStatusHasChanged and !$model->validation_status->equals(ValidationStatus::validated())) { throw ValidationException::withMessages([ 'messages' => // phpcs:ignore Generic.Files.LineLength.TooLong "L'état de livraison de la commande ne peut être modifié car la commande '{$model->id}' n'est pas validée" ]); } if ($model->getOriginal('validation_status') === ValidationStatus::cancelled()->value) { throw ValidationException::withMessages([ 'messages' => "L'état de livraison de la commande ne peut être modifié car la commande '{$model->id}' est annulée" ]); } } /** * Get updated current session order * * @return null|\App\Models\Shop\Order */ public static function getCurrentOrder(): ?self { /** @var \App\Models\Shop\Order|null $order */ if ($order = Session::get('currentOrder')) { // * Refresh order if exists in DB. $order = Order::query()->where('id', $order->id) ->orWhere('reference', $order->reference) ->first() ?? $order; } return $order; } /** * Inject the basket items into the order. * * @param \Gloudemans\Shoppingcart\Cart $basket * @return void * @throws \Illuminate\Validation\ValidationException If a product from the basket is not accessible anymore. */ public function assignBasket(Cart $basket): void { $this->basket = $basket->content(); $this->total_discounted = \intval($basket->discount()); $this->total_ttc = \intval($basket->total()); $this->total_ht = \intval($this->total_ttc - $basket->tax()); $this->total_weight = \intval($basket->weight()); } /** * Generate partial refunded items basket * * ! (diff de basket - shipped basket) * * @return \Illuminate\Support\Collection<\Gloudemans\Shoppingcart\CartItem> */ public function getPartialRefundedBasket(): Collection { $shippedBastetItems = collect($this->shipped_basket); // * FIXME: Same code here as in Bo/OrderController::prepareSubmit. // ! Ici on veux réutiliser le cart calculator (diff de basket - shipped basket). $cart = ToolboxHelper::createVirtualCart(\collect($this->basket) ->map(function (object $basketItem) use ($shippedBastetItems) { if (!$shippedBastetItems->contains('id', $basketItem->id)) { return null; } $cartItem = ToolboxHelper::basketItemObjectToCartItem($basketItem); $cartItem->associate(Product::query()->where('id', $cartItem->id)->first()); return $cartItem; })->filter()); // * Update virtual basket with actual content collect($this->shipped_basket)->each(function (object $basketItem) use ($cart) { /** @var \Gloudemans\Shoppingcart\CartItem $cartItem */ $cartItem = $cart->search(function (CartItem $cartItem) use ($basketItem) { return $cartItem->id === $basketItem->id; })->first(); $newQty = max($cartItem->qty - $basketItem->qty, 0); // * Update Qty or remove item from the cart. if (!$newQty) { $cart->remove($cartItem->rowId); return; } $cartItem->setQuantity($newQty); // ! https://github.com/bumbummen99/LaravelShoppingcart/pull/181 // * Note Lib is is actually refactoring for laravel 9 php8 only // * https://github.com/bumbummen99/LaravelShoppingcart/pull/145 // ! These lines are required to prevent amount bug. $cartItem->setTaxRate($basketItem->options->tax_rate / 100); $cartItem->setDiscountRate($basketItem->options->discount_rate); }); $virtualOrder = new self(); $virtualOrder->assignBasket($cart); return collect($virtualOrder->basket); } }