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;
}
}