HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/shop.komma.nl/app/Shipments/ShipmentService.php
<?php


namespace App\Shipments;


use App\Orders\Models\Order;
use App\Payment\PSPAdapters\AbstractPSPAdapter;
use App\Shipments\ProviderAdapters\AbstractShippingProviderAdapter;
use Carbon\Carbon;
use Komma\KMS\Core\ModelService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

class ShipmentService extends ModelService
{
    protected $modelClassName = Shipment::class;

    /**
     * Creates a shipment
     *
     * @param Order       $order
     * @param string|null $trackingCode
     * @param string|null $comment
     *
     * @return Shipment
     */
    public function create(Order $order, string $trackingCode = null, string $comment = null): Shipment
    {
        $shipment = new Shipment();
        $shipment->tracking_code = $trackingCode ?: '';
        $shipment->comment = $comment ?: '';
        $shipment->status = ShipmentStatus::NEW;
        $shipment->order()->associate($order);
        $shipment->save();

        return $shipment;
    }

    public function delete(Collection $shipments) {
        if(!self::iterableContainsShipments($shipments)) return __('KMS::shipments.general_error');

        $nonHandleableShipments = $this->nonHandleableShipments($shipments);
        if($nonHandleableShipments->count() > 0) {
            return __('KMS::shipments.non_handle_fail', ['shipment_ids' => $nonHandleableShipments->pluck('id')->join(', ')]);
        }

        $this->modelClassName::destroy($shipments->pluck('id'));
        return true;
    }

    /**
     * Returns true if the shipments status can be changed to a given status.
     * Or an an array of statuses to which you can change
     *
     * @param Shipment $shipment
     * @param string   $status
     */
    public function statusCanBeChangedTo(Shipment $shipment, string $status) {
        //Define from which status you can switch to other statuses
        $transitions = [];
        $transitions[ShipmentStatus::CANCELED] = [
        ];
        $transitions[ShipmentStatus::NEW] = [
            ShipmentStatus::READY_TO_SHIP,
            ShipmentStatus::EXCEPTION
        ];
        $transitions[ShipmentStatus::READY_TO_SHIP] = [
            ShipmentStatus::NEW,
            ShipmentStatus::EXCEPTION
        ];
        $transitions[ShipmentStatus::CARRIER_NOTIFIED] = [
            ShipmentStatus::IN_TRANSIT,
            ShipmentStatus::OUT_FOR_DELIVERY,
            ShipmentStatus::FAILED_ATTEMPT,
            ShipmentStatus::EXCEPTION
        ];
        $transitions[ShipmentStatus::IN_TRANSIT] = [
            ShipmentStatus::OUT_FOR_DELIVERY,
            ShipmentStatus::FAILED_ATTEMPT,
            ShipmentStatus::EXCEPTION,
            ShipmentStatus::DELIVERED
        ];
        $transitions[ShipmentStatus::OUT_FOR_DELIVERY] = [
            ShipmentStatus::IN_TRANSIT,
            ShipmentStatus::FAILED_ATTEMPT,
        ];
        $transitions[ShipmentStatus::FAILED_ATTEMPT] = [
            ShipmentStatus::OUT_FOR_DELIVERY,
            ShipmentStatus::FAILED_ATTEMPT,
            ShipmentStatus::EXCEPTION,
            ShipmentStatus::DELIVERED
        ];
        $transitions[ShipmentStatus::EXCEPTION] = [
            ShipmentStatus::NEW,
            ShipmentStatus::READY_TO_SHIP,
            ShipmentStatus::CARRIER_NOTIFIED,
            ShipmentStatus::IN_TRANSIT,
            ShipmentStatus::OUT_FOR_DELIVERY,
            ShipmentStatus::FAILED_ATTEMPT,
            ShipmentStatus::EXCEPTION,
            ShipmentStatus::DELIVERED
        ];
        $transitions[ShipmentStatus::DELIVERED] = [];

        if(in_array($status, $transitions[$shipment->status], true)) {
            if(!$shipment->is_announced) return true;
            else return [];
        } else {
            return $transitions[$shipment->status];
        }
    }

    public function changeStatus(Collection $shipments, string $status) {
        if(!self::iterableContainsShipments($shipments)) return __('KMS::shipments.general_error');

        foreach ($shipments as $shipment) {
            /** @var Shipment $shipment */
            $this->changeShipmentStatus($shipment, $status);
        }

        return $shipments;
    }

    /**
     * Get the shipments that the webshop won't fysically be able to handle.
     * Because they dont have the package (anymore). Because it is en-route to customer, business or somewhere in the
     * logistics change, off-premise.
     *
     * @param Collection $shipments
     *
     * @return Collection
     */
    private function nonHandleableShipments(Collection $shipments): Collection {
        if(!self::iterableContainsShipments($shipments)) return collect();
        return $shipments->filter(fn(Shipment $shipment) => $this->isNonHandleable($shipment));
    }

    /**
     * @param Shipment $shipment
     *
     * @return bool
     */
    private function isNonHandleable(Shipment $shipment): bool {
        return in_array($shipment->status, [
            ShipmentStatus::CANCELED,
            ShipmentStatus::DELIVERED,
            ShipmentStatus::FAILED_ATTEMPT,
            ShipmentStatus::IN_TRANSIT,
            ShipmentStatus::OUT_FOR_DELIVERY,
        ], true);
    }

    public function toCSVDownload(Collection $shipments = null)
    {
        $now = Carbon::now();
        return response()->streamDownload(function () use($shipments) {
            $delimiter = ';'; //Microsoft excel uses ; as the delimiter *sigh*
            $delimiterReplacement = '|'; //Whenever the delimiter is found in csv data, it will be replaced by this value.

            //Echo header
            echo implode($delimiter, [
                ucfirst(__('KMS::shipments.shipment_number')),
                ucfirst(__('KMS::shipments.tracking_code')),
                ucfirst(__('KMS::shipments.order')),
                ucfirst(__('KMS::shipments.customer')),
                ucfirst(__('KMS::shipments.status_name')),
                ucfirst(__('KMS::shipments.comment')),
            ]);
            echo PHP_EOL;

            //Write out data
            foreach($shipments as $shipment) {
                /** @var Shipment $shipment */
                $row = [];
                $row[] = $shipment->id;
                $row[] = str_replace($delimiter, $delimiterReplacement, $shipment->tracking_code);
                $row[] = str_replace($delimiter, $delimiterReplacement, $shipment->order ? $shipment->order->order_number : '-');
                $row[] = str_replace($delimiter, $delimiterReplacement, $shipment->order ? $shipment->order->customerDisplayName() : '-');
                $row[] = str_replace($delimiter, $delimiterReplacement, __('KMS::shipments.status.'.$shipment->status));
                $row[] = str_replace($delimiter, $delimiterReplacement, $shipment->comment);
                echo implode($delimiter, $row);
                echo PHP_EOL;
            }
        }, 'shipments_'.$now->format('YmdHis').'.csv');
    }

    /**
     * Get the shipments that the webshop is able to handle fysycally.
     *
     * @param Collection $shipments
     * @return Collection
     */
    private function handleableShipments(Collection $shipments): Collection {
        if(!self::iterableContainsShipments($shipments)) return collect();
        return $shipments->filter(fn(Shipment $shipment) => $this->isHandleable($shipment));
    }

    /**
     * @param Shipment $shipment
     * @return bool
     */
    private function isHandleable(Shipment $shipment): bool {
        return in_array($shipment->status, [
            ShipmentStatus::NEW,
            ShipmentStatus::READY_TO_SHIP,
            ShipmentStatus::CARRIER_NOTIFIED,
        ], true);
    }

    /**
     * Returns true if the iteratable contains shipments only. false if not.
     *
     * @param iterable $iterable
     * @return bool
     */
    public static function iterableContainsShipments(Collection $iterable):bool
    {
        foreach($iterable as $item) {
            if(!is_a($item, Shipment::class)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the latest x shipment groups. Where x is a random number.
     *
     * @param $int
     * @return Builder
     */
    public function getLatest($int): Builder
    {
        /** @var Builder $ordersQueryBuilder */
        $ordersQueryBuilder = $this->modelClassName::query();
        return $ordersQueryBuilder->latest('created_at')->limit($int);
    }

    /**
     * Retrieves an array where the key names are field names, and value are the search values.
     * And uses that array to search for shipments. The found shipments are returned as a QueryBuilder
     * that holds the query to return all the found shipments.
     *
     * @param array $input $input
     * @return Builder
     */
    public function search(array $input): Builder
    {
        /** @var Builder $queryBuilder */
        $queryBuilder = $this->modelClassName::query();

        foreach ($input as $fieldName => $searchValue) {
            if ($searchValue == '') {
                continue;
            }
            switch ($fieldName) {
                case 'id':
                case 'tracking_code':
                    $queryBuilder->where($fieldName, '=', $searchValue);
                    break;
                case 'status':
                    if ($searchValue == 'each') continue 2;
                    $queryBuilder = $queryBuilder->where('status', '=', $searchValue);
                    break;
                case 'order_number':
                    $queryBuilder = $queryBuilder->whereHas('order', function(Builder $query) use($searchValue) {
                        $query->where('order_number', '=', $searchValue);
                    });
                    break;
                case 'first_name':
                case 'last_name':
                case 'email':
                case 'shipping_street':
                case 'shipping_house_number':
                    $queryBuilder = $queryBuilder->whereHas('order', function(Builder $query) use($searchValue, $fieldName) {
                        $query->where($fieldName, 'LIKE', '%'.$searchValue.'%');
                    });
                    break;
            }
        }

        return $queryBuilder;
    }

    /**
     * @param Collection $shipments
     * @param string     $statusToChangeTo
     *
     * @return bool|\Illuminate\Http\RedirectResponse
     */
    public function shipmentsCanChangeStatusTo(Collection $shipments, string $statusToChangeTo) {
        //Build a collection of shipment ids (keys) and statuses to which they CAN change. Put those in the $shipmentStatusMessages collection
        $shipmentStatusMessages = $shipments->map(function(Shipment $shipment) use($statusToChangeTo) {
            $trueOrStatuses = $this->statusCanBeChangedTo($shipment, $statusToChangeTo);
            if($trueOrStatuses === true) return null;

            $statuses = array_map(fn($status) => __('KMS::shipments.status.'.$status), $trueOrStatuses);
            if(count($statuses) == 0) $statuses = [__('KMS::shipments.cannot_change_status')];

            return __('KMS::shipments.shipment_can_be_changed_to', ['nr' => $shipment->id, 'statuses' => implode(', ', $statuses)]);
        })->filter(fn($nullOrStatuses) => $nullOrStatuses);

        //Some shipments could not be changed to the given status. Redirect back with errors
        if($shipmentStatusMessages->count() > 0) {
            $idsAndStatusesString = __('KMS::shipments.shipment_change_status_error_intro');
            $idsAndStatusesString .= '<br>'.$shipmentStatusMessages->implode($shipmentStatusMessages, '<br>');

            return $idsAndStatusesString;
        }
        return true;
    }

    /**
     * Change the status of an order
     *
     * @param Shipment  $shipment
     * @param string $status
     *
     * @return Shipment
     */
    private function changeShipmentStatus(Shipment $shipment, string $status): Shipment
    {
        if(!ShipmentStatus::isValidItem($status)) throw new \InvalidArgumentException('The given status is invalid. Use the ShipmentStatus enum for valid statuses');

        $shipment->status = $status;
        $shipment->save();
        return $shipment;
    }
    
    public function saveFromJsonArray(array $data) {
        if(!array_key_exists('state', $data)) return new Shipment();

        $shipment = null;
        switch ($data['state']) {
            case Shipment::NEW:
                $shipment = new Shipment();
                $shipment->fill($data);
                $shipment->save();
                break;
            case Shipment::PRISTINE:
            case Shipment::DIRTY:
                /** @var Shipment $shipment */
                $shipment = Shipment::find($data['id']);
                $shipment->fill($data);
                $shipment->save();
                break;
            case Shipment::DELETED:
                $shipment = Shipment::find($data['id']);
                if(!$shipment) throw new \InvalidArgumentException('Data refers to a shipment that does not exist anymore. Cannot delete shipment');
                $this->deleteShipment($shipment);
                break;
        }

        return $shipment;
    }

    private function deleteShipment(Shipment $shipment): bool  {
        if($this->isNonHandleable($shipment)) throw new \InvalidArgumentException('Shipment is off premise. Therefore could not delete it');
        $shipment->orderedProducts()->update(['shipment_id', null]);
        $this->getAdapter()->deleteShipmentLabelDocument($shipment);
        return $shipment->delete();
    }

    /**
     * Returns the concrete adapter.
     * e.g. An adapter that extends this class.
     *
     * The adapter knows how to both work with KMS and
     * a script / api from a shipment provider to process shipments
     *
     * @return AbstractShippingProviderAdapter
     */
    public function getAdapter(): AbstractShippingProviderAdapter
    {
        $shipmentProvider = config('shipment.shipment_service_provider');

        $adapterConfigKey = 'shipment.shipment_service_providers.'.$shipmentProvider.'.adapter';
        $adapter = config($adapterConfigKey);

        if(!$adapter || !is_a($adapter, AbstractShippingProviderAdapter::class, true))
            throw new \RuntimeException('The following shipment configuration key did not exist or did not return a childclass of "'.AbstractShippingProviderAdapter::class.'": '.$adapterConfigKey);

        return new $adapter;
    }
}