diff --git a/makefile b/makefile index e631c4b02..45a6265b1 100644 --- a/makefile +++ b/makefile @@ -49,7 +49,7 @@ clean: ## Cleans all dependencies and files rm -rf ./src/Resources/app/storefront/dist/storefront # ------------------------------------------------------ rm -rf ./src/Resources/public/administration - rm -rf ./src/Resources/public/molllie-payments.js + rm -rf ./src/Resources/public/mollie-payments.js build: ## Installs the plugin, and builds the artifacts using the Shopware build commands. # ----------------------------------------------------- diff --git a/src/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderAction.php b/src/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderAction.php index 7ffe3448f..dc1a54993 100644 --- a/src/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderAction.php +++ b/src/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderAction.php @@ -2,9 +2,7 @@ namespace Kiener\MolliePayments\Compatibility\Bundles\FlowBuilder\Actions; -use Kiener\MolliePayments\Facade\MollieShipment; -use Kiener\MolliePayments\Facade\MollieShipmentInterface; -use Kiener\MolliePayments\Service\OrderService; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManagerInterface; use Kiener\MolliePayments\Service\OrderServiceInterface; use Psr\Log\LoggerInterface; use Shopware\Core\Content\Flow\Dispatching\Action\FlowAction; @@ -27,20 +25,20 @@ class ShipOrderAction extends FlowAction implements EventSubscriberInterface private $orderService; /** - * @var MollieShipmentInterface + * @var ShipmentManagerInterface */ - private $shipmentFacade; + private $shipment; /** * @param OrderServiceInterface $orderService - * @param MollieShipmentInterface $shipment + * @param ShipmentManagerInterface $shipment * @param LoggerInterface $logger */ - public function __construct(OrderServiceInterface $orderService, MollieShipmentInterface $shipment, LoggerInterface $logger) + public function __construct(OrderServiceInterface $orderService, ShipmentManagerInterface $shipment, LoggerInterface $logger) { $this->orderService = $orderService; - $this->shipmentFacade = $shipment; + $this->shipment = $shipment; $this->logger = $logger; } @@ -122,13 +120,9 @@ private function shipOrder(string $orderId, Context $context): void $this->logger->info('Starting Shipment through Flow Builder Action for order: ' . $orderNumber); - $this->shipmentFacade->shipOrder( - $order, - '', - '', - '', - $context - ); + # ship (all or) the rest of the order without providing any specific tracking information. + # this will ensure tracking data is automatically taken from the order + $this->shipment->shipOrderRest($order, null, $context); } catch (\Exception $ex) { $this->logger->error( 'Error when shipping order with Flow Builder Action', diff --git a/src/Components/MollieLimits/Service/MollieLimitsRemover.php b/src/Components/MollieLimits/Service/MollieLimitsRemover.php index 06c7ca918..622dcc4e2 100644 --- a/src/Components/MollieLimits/Service/MollieLimitsRemover.php +++ b/src/Components/MollieLimits/Service/MollieLimitsRemover.php @@ -6,6 +6,7 @@ use Kiener\MolliePayments\Exception\MissingCartServiceException; use Kiener\MolliePayments\Exception\MissingRequestException; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; +use Kiener\MolliePayments\Service\MollieApi\OrderItemsExtractor; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\Payment\Provider\ActivePaymentMethodsProviderInterface; use Kiener\MolliePayments\Service\Payment\Remover\PaymentMethodRemover; @@ -35,10 +36,10 @@ class MollieLimitsRemover extends PaymentMethodRemover * @param OrderService $orderService * @param SettingsService $settingsService * @param ActivePaymentMethodsProviderInterface $paymentMethodsProvider - * @param OrderDataExtractor $orderDataExtractor + * @param OrderItemsExtractor $orderDataExtractor * @param LoggerInterface $logger */ - public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, ActivePaymentMethodsProviderInterface $paymentMethodsProvider, OrderDataExtractor $orderDataExtractor, LoggerInterface $logger) + public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, ActivePaymentMethodsProviderInterface $paymentMethodsProvider, OrderItemsExtractor $orderDataExtractor, LoggerInterface $logger) { parent::__construct($container, $requestStack, $orderService, $settingsService, $orderDataExtractor, $logger); diff --git a/src/Components/ShipmentManager/Exceptions/NoDeliveriesFoundException.php b/src/Components/ShipmentManager/Exceptions/NoDeliveriesFoundException.php new file mode 100644 index 000000000..5edf344eb --- /dev/null +++ b/src/Components/ShipmentManager/Exceptions/NoDeliveriesFoundException.php @@ -0,0 +1,7 @@ +shopwareId = $shopwareId; + $this->quantity = $quantity; + } + + /** + * @return string + */ + public function getShopwareId(): string + { + return $this->shopwareId; + } + + /** + * @return int + */ + public function getQuantity(): int + { + return $this->quantity; + } +} diff --git a/src/Components/ShipmentManager/Models/TrackingData.php b/src/Components/ShipmentManager/Models/TrackingData.php new file mode 100644 index 000000000..b330e8782 --- /dev/null +++ b/src/Components/ShipmentManager/Models/TrackingData.php @@ -0,0 +1,58 @@ +carrier = $carrier; + $this->code = $code; + $this->trackingUrl = $trackingUrl; + } + + /** + * @return string + */ + public function getCarrier(): string + { + return $this->carrier; + } + + /** + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * @return string + */ + public function getTrackingUrl(): string + { + return $this->trackingUrl; + } +} diff --git a/src/Components/ShipmentManager/ShipmentManager.php b/src/Components/ShipmentManager/ShipmentManager.php new file mode 100644 index 000000000..b06024d08 --- /dev/null +++ b/src/Components/ShipmentManager/ShipmentManager.php @@ -0,0 +1,405 @@ +deliveryTransitionService = $deliveryTransitionService; + $this->mollieApiOrderService = $mollieApiOrderService; + $this->shipmentService = $shipmentService; + $this->orderDeliveryService = $orderDeliveryService; + $this->orderService = $orderService; + $this->orderDataExtractor = $orderDataExtractor; + $this->orderItemsExtractor = $orderItemsExtractor; + $this->trackingFactory = $trackingFactory; + } + + + /** + * @param string $orderId + * @param Context $context + * @return array + */ + public function getStatus(string $orderId, Context $context): array + { + $order = $this->orderService->getOrder($orderId, $context); + $mollieOrderId = $this->orderService->getMollieOrderId($order); + + return $this->shipmentService->getStatus($mollieOrderId, $order->getSalesChannelId()); + } + + /** + * @param string $orderId + * @param Context $context + * @return array + */ + public function getTotals(string $orderId, Context $context): array + { + $order = $this->orderService->getOrder($orderId, $context); + $mollieOrderId = $this->orderService->getMollieOrderId($order); + + return $this->shipmentService->getTotals($mollieOrderId, $order->getSalesChannelId()); + } + + /** + * @param OrderEntity $order + * @param null|TrackingData $tracking + * @param array $shippingItems + * @param Context $context + * @throws NoDeliveriesFoundException + * @throws NoLineItemsProvidedException + * @return \Mollie\Api\Resources\Shipment + */ + public function shipOrder(OrderEntity $order, ?TrackingData $tracking, array $shippingItems, Context $context): \Mollie\Api\Resources\Shipment + { + if (empty($shippingItems)) { + throw new NoLineItemsProvidedException('Please provide a valid list of line items that should be shipped!'); + } + + + if ($tracking instanceof TrackingData) { + $trackingData = $this->trackingFactory->create( + $tracking->getCarrier(), + $tracking->getCode(), + $tracking->getTrackingUrl() + ); + } else { + $trackingData = $this->trackingFactory->trackingFromOrder($order); + } + + + $orderAttr = new OrderAttributes($order); + + $mollieOrderId = $orderAttr->getMollieOrderId(); + + $mollieShippingItems = []; + + # we have to look up our Mollie LineItem IDs from the order line items. + # so we iterate through both of our lists and search it + $orderLineItems = $order->getLineItems(); + + if ($orderLineItems instanceof OrderLineItemCollection) { + foreach ($shippingItems as $shippingItem) { + foreach ($orderLineItems as $orderLineItem) { + # now search the order line item by our provided shopware ID + if ($orderLineItem->getId() === $shippingItem->getShopwareId()) { + + # extract the Mollie order line ID from our custom fields + $attr = new OrderLineItemEntityAttributes($orderLineItem); + $mollieID = $attr->getMollieOrderLineID(); + + $mollieShippingItems[] = new MollieShippingItem( + $mollieID, + $shippingItem->getQuantity() + ); + + break; + } + } + } + } + + $shipment = $this->shipmentService->shipOrder( + $mollieOrderId, + $order->getSalesChannelId(), + $mollieShippingItems, + $trackingData + ); + + # -------------------------------------------------------------------------------------- + # post-shipping processing + + $this->transitionOrder($order, $mollieOrderId, $context); + + $this->markDeliveryCustomFields($order, $context); + + return $shipment; + } + + /** + * @param OrderEntity $order + * @param null|TrackingData $tracking + * @param Context $context + * @throws \Exception + * @return \Mollie\Api\Resources\Shipment + */ + public function shipOrderRest(OrderEntity $order, ?TrackingData $tracking, Context $context): \Mollie\Api\Resources\Shipment + { + if ($tracking instanceof TrackingData) { + $trackingData = $this->trackingFactory->create( + $tracking->getCarrier(), + $tracking->getCode(), + $tracking->getTrackingUrl() + ); + } else { + $trackingData = $this->trackingFactory->trackingFromOrder($order); + } + + $orderAttr = new OrderAttributes($order); + + $mollieOrderId = $orderAttr->getMollieOrderId(); + + # ship order with empty array + # so that the Mollie shipAll is being triggered + # which always ships everything or just the rest + $shipment = $this->shipmentService->shipOrder( + $mollieOrderId, + $order->getSalesChannelId(), + [], + $trackingData + ); + + # -------------------------------------------------------------------------------------- + # post-shipping processing + + $this->transitionOrder($order, $mollieOrderId, $context); + + $this->markDeliveryCustomFields($order, $context); + + return $shipment; + } + + /** + * @param OrderEntity $order + * @param string $itemIdentifier + * @param int $quantity + * @param null|TrackingData $tracking + * @param Context $context + * @throws \Exception + * @return \Mollie\Api\Resources\Shipment + */ + public function shipItem(OrderEntity $order, string $itemIdentifier, int $quantity, ?TrackingData $tracking, Context $context): \Mollie\Api\Resources\Shipment + { + $mollieOrderId = $this->orderService->getMollieOrderId($order); + + $lineItems = $this->findMatchingLineItems($order, $itemIdentifier, $context); + + if ($lineItems->count() > 1) { + throw new OrderLineItemFoundManyException($itemIdentifier); + } + + $lineItem = $lineItems->first(); + unset($lineItems); + + if (!$lineItem instanceof OrderLineItemEntity) { + throw new OrderLineItemNotFoundException($itemIdentifier); + } + + + if ($tracking instanceof TrackingData) { + $mollieTracking = $this->trackingFactory->create( + $tracking->getCarrier(), + $tracking->getCode(), + $tracking->getTrackingUrl() + ); + } else { + $mollieTracking = $this->trackingFactory->trackingFromOrder($order); + } + + $mollieOrderLineId = $this->orderService->getMollieOrderLineId($lineItem); + + # if we did not provide a quantity + # we ship everything that is left and shippable + if ($quantity === 0) { + $quantity = $this->mollieApiOrderService->getMollieOrderLine( + $mollieOrderId, + $mollieOrderLineId, + $order->getSalesChannelId() + )->shippableQuantity; + } + + $shipment = $this->shipmentService->shipItem( + $mollieOrderId, + $order->getSalesChannelId(), + $mollieOrderLineId, + $quantity, + $mollieTracking + ); + + # -------------------------------------------------------------------------------------- + # post-shipping processing + + $this->transitionOrder($order, $mollieOrderId, $context); + + $this->markDeliveryCustomFields($order, $context); + + return $shipment; + } + + /** + * @param OrderEntity $order + * @param string $mollieOrderId + * @param Context $context + * @return void + */ + private function transitionOrder(OrderEntity $order, string $mollieOrderId, Context $context): void + { + $delivery = $this->orderDataExtractor->extractDelivery($order, $context); + + # we need to see if our order is now "complete" + # if its complete it can be marked as fully shipped + # if not, then its only partially shipped + $mollieOrder = $this->mollieApiOrderService->getMollieOrder($mollieOrderId, $order->getSalesChannelId()); + + if ($mollieOrder->status === MollieStatus::COMPLETED) { + $this->deliveryTransitionService->shipDelivery($delivery, $context); + } else { + $this->deliveryTransitionService->partialShipDelivery($delivery, $context); + } + } + + /** + * @param OrderEntity $order + * @param Context $context + * @return void + */ + private function markDeliveryCustomFields(OrderEntity $order, Context $context) + { + $deliveries = $order->getDeliveries(); + + if (!$deliveries instanceof OrderDeliveryCollection) { + return; + } + + foreach ($deliveries as $delivery) { + $values = [ + CustomFieldsInterface::DELIVERY_SHIPPED => true + ]; + + $this->orderDeliveryService->updateCustomFields($delivery, $values, $context); + } + } + + /** + * Try to find lineItems matching the $itemIdentifier. Shopware does not have a unique human-readable identifier for + * order line items, so we have to check for several fields, like product number or the mollie order line id. + * + * @param OrderEntity $order + * @param string $itemIdentifier + * @param Context $context + * @return OrderLineItemCollection + */ + private function findMatchingLineItems(OrderEntity $order, string $itemIdentifier, Context $context): OrderLineItemCollection + { + return $this->orderItemsExtractor->extractLineItems($order)->filter(function ($lineItem) use ($itemIdentifier) { + /** @var OrderLineItemEntity $lineItem */ + + // Default Shopware: If the lineItem is of type "product" and has an associated ProductEntity, + // check if the itemIdentifier matches the product's product number. + if ($lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE && + $lineItem->getProduct() instanceof ProductEntity && + $lineItem->getProduct()->getProductNumber() === $itemIdentifier) { + return true; + } + + // If it's not a "product" type lineItem, for example if it's a completely custom lineItem type, + // check if the payload has a productNumber in it that matches the itemIdentifier. + if (!empty($lineItem->getPayload()) && + array_key_exists('productNumber', $lineItem->getPayload()) && + $lineItem->getPayload()['productNumber'] === $itemIdentifier) { + return true; + } + + // Check itemIdentifier against the mollie order_line_id custom field + $customFields = $lineItem->getCustomFields() ?? []; + $mollieOrderLineId = $customFields[CustomFieldsInterface::MOLLIE_KEY]['order_line_id'] ?? null; + if (!is_null($mollieOrderLineId) && $mollieOrderLineId === $itemIdentifier) { + return true; + } + + // If it hasn't passed any of the above tests, check if the itemIdentifier is a valid Uuid... + if (!Uuid::isValid($itemIdentifier)) { + return false; + } + + // ... and then check if it matches the Id of the entity the lineItem is referencing, + // or if it matches the Id of the lineItem itself. + if ($lineItem->getReferencedId() === $itemIdentifier || $lineItem->getId() === $itemIdentifier) { + return true; + } + + // Otherwise, this lineItem does not match the itemIdentifier at all. + return false; + }); + } +} diff --git a/src/Components/ShipmentManager/ShipmentManagerInterface.php b/src/Components/ShipmentManager/ShipmentManagerInterface.php new file mode 100644 index 000000000..4e19520d2 --- /dev/null +++ b/src/Components/ShipmentManager/ShipmentManagerInterface.php @@ -0,0 +1,54 @@ + + */ + public function getStatus(string $orderId, Context $context): array; + + /** + * @param string $orderId + * @param Context $context + * @return array + */ + public function getTotals(string $orderId, Context $context): array; + + /** + * @param OrderEntity $order + * @param null|TrackingData $tracking + * @param ShipmentLineItem[] $shippingItems + * @param Context $context + * @return Shipment + */ + public function shipOrder(OrderEntity $order, ?TrackingData $tracking, array $shippingItems, Context $context): Shipment; + + /** + * @param OrderEntity $order + * @param null|TrackingData $tracking + * @param Context $context + * @return Shipment + */ + public function shipOrderRest(OrderEntity $order, ?TrackingData $tracking, Context $context): Shipment; + + /** + * @param OrderEntity $order + * @param string $itemIdentifier + * @param int $quantity + * @param null|TrackingData $tracking + * @param Context $context + * @return Shipment + */ + public function shipItem(OrderEntity $order, string $itemIdentifier, int $quantity, ?TrackingData $tracking, Context $context): Shipment; +} diff --git a/src/Components/Subscription/Services/PaymentMethodRemover/SubscriptionRemover.php b/src/Components/Subscription/Services/PaymentMethodRemover/SubscriptionRemover.php index b33329d26..4d81d3131 100644 --- a/src/Components/Subscription/Services/PaymentMethodRemover/SubscriptionRemover.php +++ b/src/Components/Subscription/Services/PaymentMethodRemover/SubscriptionRemover.php @@ -3,6 +3,7 @@ namespace Kiener\MolliePayments\Components\Subscription\Services\PaymentMethodRemover; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; +use Kiener\MolliePayments\Service\MollieApi\OrderItemsExtractor; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\Payment\Remover\PaymentMethodRemover; use Kiener\MolliePayments\Service\SettingsService; @@ -39,10 +40,10 @@ class SubscriptionRemover extends PaymentMethodRemover * @param SettingsService $pluginSettings * @param OrderService $orderService * @param SettingsService $settingsService - * @param OrderDataExtractor $orderDataExtractor + * @param OrderItemsExtractor $orderDataExtractor * @param LoggerInterface $logger */ - public function __construct(ContainerInterface $container, RequestStack $requestStack, SettingsService $pluginSettings, OrderService $orderService, SettingsService $settingsService, OrderDataExtractor $orderDataExtractor, LoggerInterface $logger) + public function __construct(ContainerInterface $container, RequestStack $requestStack, SettingsService $pluginSettings, OrderService $orderService, SettingsService $settingsService, OrderItemsExtractor $orderDataExtractor, LoggerInterface $logger) { parent::__construct($container, $requestStack, $orderService, $settingsService, $orderDataExtractor, $logger); diff --git a/src/Components/Voucher/Service/VoucherRemover.php b/src/Components/Voucher/Service/VoucherRemover.php index 74e793273..31142b87f 100644 --- a/src/Components/Voucher/Service/VoucherRemover.php +++ b/src/Components/Voucher/Service/VoucherRemover.php @@ -5,6 +5,7 @@ use Kiener\MolliePayments\Service\Cart\Voucher\VoucherCartCollector; use Kiener\MolliePayments\Service\Cart\Voucher\VoucherService; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; +use Kiener\MolliePayments\Service\MollieApi\OrderItemsExtractor; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\Payment\Remover\PaymentMethodRemover; use Kiener\MolliePayments\Service\SettingsService; @@ -34,10 +35,10 @@ class VoucherRemover extends PaymentMethodRemover * @param OrderService $orderService * @param SettingsService $settingsService * @param VoucherService $voucherService - * @param OrderDataExtractor $orderDataExtractor + * @param OrderItemsExtractor $orderDataExtractor * @param LoggerInterface $logger */ - public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, VoucherService $voucherService, OrderDataExtractor $orderDataExtractor, LoggerInterface $logger) + public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, VoucherService $voucherService, OrderItemsExtractor $orderDataExtractor, LoggerInterface $logger) { parent::__construct($container, $requestStack, $orderService, $settingsService, $orderDataExtractor, $logger); diff --git a/src/Controller/Api/Order/ShippingControllerBase.php b/src/Controller/Api/Order/ShippingControllerBase.php index 2796f58f6..159d37d3b 100644 --- a/src/Controller/Api/Order/ShippingControllerBase.php +++ b/src/Controller/Api/Order/ShippingControllerBase.php @@ -3,18 +3,24 @@ namespace Kiener\MolliePayments\Controller\Api\Order; use Exception; -use Kiener\MolliePayments\Facade\MollieShipment; +use Kiener\MolliePayments\Components\ShipmentManager\Models\ShipmentLineItem; +use Kiener\MolliePayments\Components\ShipmentManager\Models\TrackingData; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManager; +use Kiener\MolliePayments\Service\OrderService; +use Kiener\MolliePayments\Struct\OrderLineItemEntity\OrderLineItemEntityAttributes; use Kiener\MolliePayments\Traits\Api\ApiTrait; use Mollie\Api\Resources\OrderLine; use Mollie\Api\Resources\Shipment; use Psr\Log\LoggerInterface; +use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection; +use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; use Shopware\Core\Framework\ShopwareHttpException; use Shopware\Core\Framework\Validation\DataBag\QueryDataBag; use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; class ShippingControllerBase extends AbstractController @@ -22,56 +28,291 @@ class ShippingControllerBase extends AbstractController use ApiTrait; /** - * @var MollieShipment + * @var ShipmentManager */ - private $shipmentFacade; + private $shipment; + + /** + * @var OrderService + */ + private $orderService; /** * @var LoggerInterface */ private $logger; + /** - * @param MollieShipment $shipmentFacade + * @param ShipmentManager $shipmentFacade + * @param OrderService $orderService * @param LoggerInterface $logger */ - public function __construct(MollieShipment $shipmentFacade, LoggerInterface $logger) + public function __construct(ShipmentManager $shipmentFacade, OrderService $orderService, LoggerInterface $logger) { - $this->shipmentFacade = $shipmentFacade; + $this->shipment = $shipmentFacade; + $this->orderService = $orderService; $this->logger = $logger; } + /** - * @Route("/api/mollie/ship/order", name="api.mollie.ship.order", methods={"GET"}) + * @Route("/api/_action/mollie/ship/status", name="api.action.mollie.ship.status", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context + * @return JsonResponse + */ + public function status(RequestDataBag $data, Context $context): JsonResponse + { + return $this->getStatusResponse($data->get('orderId'), $context); + } + + /** + * @Route("/api/v{version}/_action/mollie/ship/status", name="api.action.mollie.ship.status.legacy", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context + * @return JsonResponse + */ + public function statusLegacy(RequestDataBag $data, Context $context): JsonResponse + { + return $this->getStatusResponse($data->get('orderId'), $context); + } + + /** + * @Route("/api/_action/mollie/ship/total", name="api.action.mollie.ship.total", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context + * @return JsonResponse + */ + public function total(RequestDataBag $data, Context $context): JsonResponse + { + return $this->getTotalResponse($data->get('orderId'), $context); + } + + /** + * @Route("/api/v{version}/_action/mollie/ship/total", name="api.action.mollie.ship.total.legacy", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context + * @return JsonResponse + */ + public function totalLegacy(RequestDataBag $data, Context $context): JsonResponse + { + return $this->getTotalResponse($data->get('orderId'), $context); + } + + + /** + * This is the custom operational route for shipping using the API. + * This shipment is based on ship all or rest of items automatically. + * It can be used by 3rd parties, ERP systems and more. + * + * @Route("/api/mollie/ship/order", name="api.mollie.ship.order", methods={"POST"}) + * + * @param Request $request + * @param Context $context + * @return JsonResponse + */ + public function shipOrderOperational(Request $request, Context $context): JsonResponse + { + $orderNumber = ''; + $trackingCarrier = ''; + $trackingCode = ''; + $trackingUrl = ''; + + try { + $content = (string)$request->getContent(); + $jsonData = json_decode($content, true); + + $orderNumber = (string)$jsonData['orderNumber']; + $trackingCarrier = (string)$jsonData['trackingCarrier']; + $trackingCode = (string)$jsonData['trackingCode']; + $trackingUrl = (string)$jsonData['trackingUrl']; + + if ($orderNumber === '') { + throw new \InvalidArgumentException('Missing Argument for Order Number!'); + } + + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipOrderRest( + $order, + $tracking, + $context + ); + + return $this->shipmentToJson($shipment); + } catch (\Exception $e) { + + $this->logger->error( + 'Error when shipping order: ' . $orderNumber, + [ + 'error' => $e + ] + ); + + $data = [ + 'orderNumber' => $orderNumber, + 'trackingCarrier' => $trackingCarrier, + 'trackingCode' => $trackingCode, + 'trackingUrl' => $trackingUrl, + ]; + + return $this->exceptionToJson($e, $data); + } + } + + /** + * This is the custom operational route for shipping using the API. + * This shipment is based on ship all or rest of items automatically. + * It can be used by 3rd parties, ERP systems and more. + * This comes without tracking information. Please use the POST version. + * + * @Route("/api/mollie/ship/order", name="api.mollie.ship.order.deprecated", methods={"GET"}) * * @param QueryDataBag $query * @param Context $context * @return JsonResponse */ - public function shipOrderApi(QueryDataBag $query, Context $context): JsonResponse + public function shipOrderOperationalDeprecated(QueryDataBag $query, Context $context): JsonResponse { + $orderNumber = ''; + try { $orderNumber = $query->get('number'); - $trackingCarrier = $query->get('trackingCarrier', ''); - $trackingCode = $query->get('trackingCode', ''); - $trackingUrl = $query->get('trackingUrl', ''); if ($orderNumber === null) { throw new \InvalidArgumentException('Missing Argument for Order Number!'); } - $shipment = $this->shipmentFacade->shipOrderByOrderNumber( - $orderNumber, - $trackingCarrier, - $trackingCode, - $trackingUrl, + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + $shipment = $this->shipment->shipOrderRest( + $order, + null, + $context + ); + + return $this->shipmentToJson($shipment); + } catch (\Exception $e) { + + $this->logger->error( + 'Error when shipping order (deprecated): ' . $orderNumber, + [ + 'error' => $e + ] + ); + + $data = [ + 'orderNumber' => $orderNumber, + ]; + + return $this->exceptionToJson($e, $data); + } + } + + /** + * This is the custom operational route for batch shipping of orders using the API. + * This shipment requires a valid list of line items to be provided. + * It can be used by 3rd parties, ERP systems and more. + * + * @Route("/api/mollie/ship/order/batch", name="api.mollie.ship.order.batch", methods={"POST"}) + * + * @param Request $request + * @param Context $context + * @return JsonResponse + */ + public function shipOrderBatchOperational(Request $request, Context $context): JsonResponse + { + $orderNumber = ''; + $requestItems = []; + $trackingCarrier = ''; + $trackingCode = ''; + $trackingUrl = ''; + + try { + $content = (string)$request->getContent(); + $jsonData = json_decode($content, true); + + $orderNumber = (string)$jsonData['orderNumber']; + $requestItems = $jsonData['items']; + $trackingCarrier = (string)$jsonData['trackingCarrier']; + $trackingCode = (string)$jsonData['trackingCode']; + $trackingUrl = (string)$jsonData['trackingUrl']; + + if (!is_array($requestItems)) { + $requestItems = []; + } + + if ($orderNumber === '') { + throw new \InvalidArgumentException('Missing Argument for Order Number!'); + } + + if (empty($requestItems)) { + throw new \InvalidArgumentException('Missing Argument for Items!'); + } + + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + $orderItems = $order->getLineItems(); + + if (!$orderItems instanceof OrderLineItemCollection) { + throw new Exception('Shopware order does not have any line requestItems!'); + } + + $shipmentItems = []; + + # we need to look up the internal line item ids for the order + # because we are only provided product numbers + foreach ($orderItems as $orderItem) { + foreach ($requestItems as $requestItem) { + $orderItemAttr = new OrderLineItemEntityAttributes($orderItem); + + $productNumber = $requestItem['productNumber']; + $quantity = $requestItem['quantity']; + + # check if we have found our product by number + if ($orderItemAttr->getProductNumber() === $productNumber) { + $shipmentItems[] = new ShipmentLineItem( + $orderItem->getId(), + $quantity + ); + break; + } + } + } + + if (empty($shipmentItems)) { + throw new \InvalidArgumentException('Provided items have not been found in order!'); + } + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipOrder( + $order, + $tracking, + $shipmentItems, $context ); return $this->shipmentToJson($shipment); } catch (\Exception $e) { + + $this->logger->error( + 'Error when shipping batch order: ' . $orderNumber, + [ + 'error' => $e + ] + ); + $data = [ 'orderNumber' => $orderNumber, + 'items' => $requestItems, 'trackingCarrier' => $trackingCarrier, 'trackingCode' => $trackingCode, 'trackingUrl' => $trackingUrl, @@ -82,44 +323,66 @@ public function shipOrderApi(QueryDataBag $query, Context $context): JsonRespons } /** - * @Route("/api/mollie/ship/item", name="api.mollie.ship.item", methods={"GET"}) + * This is the custom operational route for shipping items using the API. + * It can be used by 3rd parties, ERP systems and more. * - * @param QueryDataBag $query + * @Route("/api/mollie/ship/item", name="api.mollie.ship.item", methods={"POST"}) + * + * @param Request $request * @param Context $context - * @throws \Exception * @return JsonResponse + * @throws \Exception */ - public function shipItemApi(QueryDataBag $query, Context $context): JsonResponse + public function shipItemOperational(Request $request, Context $context): JsonResponse { + $orderNumber = ''; + $itemIdentifier = ''; + $quantity = ''; + $trackingCarrier = ''; + $trackingCode = ''; + $trackingUrl = ''; + try { - $orderNumber = $query->get('order'); - $itemIdentifier = $query->get('item'); - $quantity = $query->getInt('quantity'); - $trackingCarrier = $query->get('trackingCarrier', ''); - $trackingCode = $query->get('trackingCode', ''); - $trackingUrl = $query->get('trackingUrl', ''); + $content = (string)$request->getContent(); + $jsonData = json_decode($content, true); + $orderNumber = (string)$jsonData['orderNumber']; + $itemProductNumber = (string)$jsonData['productNumber']; + $quantity = (int)$jsonData['quantity']; + $trackingCarrier = (string)$jsonData['trackingCarrier']; + $trackingCode = (string)$jsonData['trackingCode']; + $trackingUrl = (string)$jsonData['trackingUrl']; - if ($orderNumber === null) { + if ($orderNumber === '') { throw new \InvalidArgumentException('Missing Argument for Order Number!'); } - if ($itemIdentifier === null) { - throw new \InvalidArgumentException('Missing Argument for Item identifier!'); + if ($itemProductNumber === '') { + throw new \InvalidArgumentException('Missing Argument for item product number!'); } - $shipment = $this->shipmentFacade->shipItemByOrderNumber( - $orderNumber, - $itemIdentifier, + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipItem( + $order, + $itemProductNumber, $quantity, - $trackingCarrier, - $trackingCode, - $trackingUrl, + $tracking, $context ); return $this->shipmentToJson($shipment); } catch (\Exception $e) { + + $this->logger->error( + 'Error when shipping item of order: ' . $orderNumber, + [ + 'error' => $e + ] + ); + $data = [ 'orderNumber' => $orderNumber, 'item' => $itemIdentifier, @@ -133,160 +396,264 @@ public function shipItemApi(QueryDataBag $query, Context $context): JsonResponse } } - private function shipmentToJson(Shipment $shipment): JsonResponse + /** + * This is the custom operational route for shipping items using the API. + * It can be used by 3rd parties, ERP systems and more. + * This comes without tracking information. Please use the POST version. + * + * @Route("/api/mollie/ship/item", name="api.mollie.ship.item.deprecated", methods={"GET"}) + * + * @param QueryDataBag $query + * @param Context $context + * @return JsonResponse + * @throws \Exception + */ + public function shipItemOperationalDeprecated(QueryDataBag $query, Context $context): JsonResponse { - $lines = []; - /** @var OrderLine $orderLine */ - foreach ($shipment->lines() as $orderLine) { - $lines[] = [ - 'id' => $orderLine->id, - 'orderId' => $orderLine->orderId, - 'name' => $orderLine->name, - 'sku' => $orderLine->sku, - 'type' => $orderLine->type, - 'status' => $orderLine->status, - 'quantity' => $orderLine->quantity, - 'unitPrice' => (array)$orderLine->unitPrice, - 'vatRate' => $orderLine->vatRate, - 'vatAmount' => (array)$orderLine->vatAmount, - 'totalAmount' => (array)$orderLine->totalAmount, - 'createdAt' => $orderLine->createdAt + $orderNumber = ''; + $itemIdentifier = ''; + $quantity = ''; + + try { + $orderNumber = $query->get('order'); + $itemIdentifier = $query->get('item'); + $quantity = $query->getInt('quantity'); + + if ($orderNumber === '') { + throw new \InvalidArgumentException('Missing argument for Order Number!'); + } + + if ($itemIdentifier === '') { + throw new \InvalidArgumentException('Missing argument for Item identifier! Please provide a product number!'); + } + + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + $shipment = $this->shipment->shipItem( + $order, + $itemIdentifier, + $quantity, + null, + $context + ); + + return $this->shipmentToJson($shipment); + } catch (\Exception $e) { + + $this->logger->error( + 'Error when shipping item of order (deprecated): ' . $orderNumber, + [ + 'error' => $e + ] + ); + + $data = [ + 'orderNumber' => $orderNumber, + 'item' => $itemIdentifier, + 'quantity' => $quantity, ]; - } - return $this->json([ - 'id' => $shipment->id, - 'orderId' => $shipment->orderId, - 'createdAt' => $shipment->createdAt, - 'lines' => $lines, - 'tracking' => $shipment->tracking - ]); + return $this->exceptionToJson($e, $data); + } } /** - * @param Exception $e - * @param array $additionalData + * This is the plain action API route that is used in the Shopware Administration. + * + * @Route("/api/_action/mollie/ship", name="api.action.mollie.ship.order", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context * @return JsonResponse */ - private function exceptionToJson(Exception $e, array $additionalData = []): JsonResponse + public function shipOrderAdmin(RequestDataBag $data, Context $context): JsonResponse { - $this->logger->error( - $e->getMessage(), - $additionalData - ); + $orderId = $data->getAlnum('orderId'); + $trackingCarrier = $data->get('trackingCarrier', ''); + $trackingCode = $data->get('trackingCode', ''); + $trackingUrl = $data->get('trackingUrl', ''); + $itemsBag = $data->get('items', []); + + $items = []; + if ($itemsBag instanceof RequestDataBag) { + $items = $itemsBag->all(); + } - return $this->json([ - 'error' => get_class($e), - 'message' => $e->getMessage(), - 'data' => $additionalData - ], 400); + return $this->processAdminShipOrder( + $orderId, + $trackingCarrier, + $trackingCode, + $trackingUrl, + $items, + $context + ); } - // Admin routes + /** + * @Route("/api/v{version}/_action/mollie/ship", name="api.action.mollie.ship.order.legacy", methods={"POST"}) + * + * @param RequestDataBag $data + * @param Context $context + * @return JsonResponse + */ + public function shipOrderAdminLegacy(RequestDataBag $data, Context $context): JsonResponse + { + $orderId = $data->getAlnum('orderId'); + $trackingCarrier = $data->get('trackingCarrier', ''); + $trackingCode = $data->get('trackingCode', ''); + $trackingUrl = $data->get('trackingUrl', ''); + $itemsBag = $data->get('items', []); + + $items = []; + if ($itemsBag instanceof RequestDataBag) { + $items = $itemsBag->all(); + } + + return $this->processAdminShipOrder( + $orderId, + $trackingCarrier, + $trackingCode, + $trackingUrl, + $items, + $context + ); + } /** - * @Route("/api/_action/mollie/ship", name="api.action.mollie.ship.order", methods={"POST"}) + * This is the plain action API route that is used in the Shopware Administration. + * + * @Route("/api/_action/mollie/ship/item", name="api.action.mollie.ship.item", methods={"POST"}) * * @param RequestDataBag $data * @param Context $context * @return JsonResponse */ - public function shipOrder(RequestDataBag $data, Context $context): JsonResponse + public function shipItemAdmin(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipOrderResponse( - $data->getAlnum('orderId'), - $data->get('trackingCarrier', ''), - $data->get('trackingCode', ''), - $data->get('trackingUrl', ''), + $orderId = $data->getAlnum('orderId'); + $itemId = $data->get('itemId', ''); + $quantity = $data->get('quantity', 0); + $trackingCarrier = $data->get('trackingCarrier', ''); + $trackingCode = $data->get('trackingCode', ''); + $trackingUrl = $data->get('trackingUrl', ''); + + return $this->processShipItem( + $orderId, + $itemId, + $quantity, + $trackingCarrier, + $trackingCode, + $trackingUrl, $context ); } /** - * @Route("/api/v{version}/_action/mollie/ship", name="api.action.mollie.ship.order.legacy", methods={"POST"}) + * @Route("/api/v{version}/_action/mollie/ship/item", name="api.action.mollie.ship.item.legacy", methods={"POST"}) * * @param RequestDataBag $data * @param Context $context * @return JsonResponse */ - public function shipOrderLegacy(RequestDataBag $data, Context $context): JsonResponse + public function shipItemAdminLegacy(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipOrderResponse( - $data->getAlnum('orderId'), - $data->get('trackingCarrier', ''), - $data->get('trackingCode', ''), - $data->get('trackingUrl', ''), + $orderId = $data->getAlnum('orderId'); + $itemId = $data->get('itemId', ''); + $quantity = $data->get('quantity', 0); + $trackingCarrier = $data->get('trackingCarrier', ''); + $trackingCode = $data->get('trackingCode', ''); + $trackingUrl = $data->get('trackingUrl', ''); + + return $this->processShipItem( + $orderId, + $itemId, + $quantity, + $trackingCarrier, + $trackingCode, + $trackingUrl, $context ); } + /** * @param string $orderId - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl * @param Context $context * @return JsonResponse */ - public function getShipOrderResponse(string $orderId, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): JsonResponse + private function getTotalResponse(string $orderId, Context $context): JsonResponse { try { - if (empty($orderId)) { - throw new \InvalidArgumentException('Missing Argument for Order ID!'); - } - - $shipment = $this->shipmentFacade->shipOrderByOrderId( - $orderId, - $trackingCarrier, - $trackingCode, - $trackingUrl, - $context - ); - - return $this->shipmentToJson($shipment); - } catch (\Exception $e) { - return $this->buildErrorResponse($e->getMessage()); + $totals = $this->shipment->getTotals($orderId, $context); + } catch (ShopwareHttpException $e) { + $this->logger->error($e->getMessage()); + return $this->json(['message' => $e->getMessage()], $e->getStatusCode()); + } catch (\Throwable $e) { + $this->logger->error($e->getMessage()); + return $this->json(['message' => $e->getMessage()], 500); } + + return $this->json($totals); } /** - * @Route("/api/_action/mollie/ship/item", name="api.action.mollie.ship.item", methods={"POST"}) - * - * @param RequestDataBag $data + * @param string $orderId * @param Context $context * @return JsonResponse */ - public function shipItem(RequestDataBag $data, Context $context): JsonResponse + private function getStatusResponse(string $orderId, Context $context): JsonResponse { - return $this->getShipItemResponse( - $data->getAlnum('orderId'), - $data->getAlnum('itemId'), - $data->getInt('quantity'), - $data->get('trackingCarrier', ''), - $data->get('trackingCode', ''), - $data->get('trackingUrl', ''), - $context - ); + try { + $status = $this->shipment->getStatus($orderId, $context); + } catch (ShopwareHttpException $e) { + $this->logger->error($e->getMessage()); + return $this->json(['message' => $e->getMessage()], $e->getStatusCode()); + } catch (\Throwable $e) { + $this->logger->error($e->getMessage()); + return $this->json(['message' => $e->getMessage()], 500); + } + + return $this->json($status); } /** - * @Route("/api/v{version}/_action/mollie/ship/item", name="api.action.mollie.ship.item.legacy", methods={"POST"}) - * - * @param RequestDataBag $data + * @param string $orderId + * @param string $trackingCarrier + * @param string $trackingCode + * @param string $trackingUrl + * @param array $lineItems * @param Context $context * @return JsonResponse */ - public function shipItemLegacy(RequestDataBag $data, Context $context): JsonResponse + private function processAdminShipOrder(string $orderId, string $trackingCarrier, string $trackingCode, string $trackingUrl, array $lineItems, Context $context): JsonResponse { - return $this->getShipItemResponse( - $data->getAlnum('orderId'), - $data->getAlnum('itemId'), - $data->getInt('quantity'), - $data->get('trackingCarrier', ''), - $data->get('trackingCode', ''), - $data->get('trackingUrl', ''), - $context - ); + try { + if (empty($orderId)) { + throw new \InvalidArgumentException('Missing Argument for Order ID!'); + } + + $order = $this->orderService->getOrder($orderId, $context); + + if (!$order instanceof OrderEntity) { + throw new \InvalidArgumentException('Order with ID: ' . $orderId . ' not found!'); + } + + # hydrate to our real item struct + $items = $this->hydrateShippingItems($lineItems); + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipOrder( + $order, + $tracking, + $items, + $context + ); + + return $this->shipmentToJson($shipment); + } catch (\Exception $e) { + return $this->buildErrorResponse($e->getMessage()); + } } /** @@ -299,15 +666,8 @@ public function shipItemLegacy(RequestDataBag $data, Context $context): JsonResp * @param Context $context * @return JsonResponse */ - public function getShipItemResponse( - string $orderId, - string $itemId, - int $quantity, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): JsonResponse { + private function processShipItem(string $orderId, string $itemId, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): JsonResponse + { try { if (empty($orderId)) { throw new \InvalidArgumentException('Missing Argument for Order ID!'); @@ -317,13 +677,19 @@ public function getShipItemResponse( throw new \InvalidArgumentException('Missing Argument for Item ID!'); } - $shipment = $this->shipmentFacade->shipItemByOrderId( - $orderId, + $order = $this->orderService->getOrder($orderId, $context); + + if (!$order instanceof OrderEntity) { + throw new \InvalidArgumentException('Order with id: ' . $orderId . ' not found!'); + } + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipItem( + $order, $itemId, $quantity, - $trackingCarrier, - $trackingCode, - $trackingUrl, + $tracking, $context ); @@ -343,90 +709,70 @@ public function getShipItemResponse( } /** - * @Route("/api/_action/mollie/ship/status", name="api.action.mollie.ship.status", methods={"POST"}) - * - * @param RequestDataBag $data - * @param Context $context + * @param Shipment $shipment * @return JsonResponse */ - public function status(RequestDataBag $data, Context $context): JsonResponse - { - return $this->getStatusResponse($data->get('orderId'), $context); - } - - /** - * @Route("/api/v{version}/_action/mollie/ship/status", name="api.action.mollie.ship.status.legacy", methods={"POST"}) - * - * @param RequestDataBag $data - * @param Context $context - * @return JsonResponse - */ - public function statusLegacy(RequestDataBag $data, Context $context): JsonResponse - { - return $this->getStatusResponse($data->get('orderId'), $context); - } - - /** - * @param string $orderId - * @param Context $context - * @return JsonResponse - */ - public function getStatusResponse(string $orderId, Context $context): JsonResponse + private function shipmentToJson(Shipment $shipment): JsonResponse { - try { - $status = $this->shipmentFacade->getStatus($orderId, $context); - } catch (ShopwareHttpException $e) { - $this->logger->error($e->getMessage()); - return $this->json(['message' => $e->getMessage()], $e->getStatusCode()); - } catch (\Throwable $e) { - $this->logger->error($e->getMessage()); - return $this->json(['message' => $e->getMessage()], 500); + $lines = []; + /** @var OrderLine $orderLine */ + foreach ($shipment->lines() as $orderLine) { + $lines[] = [ + 'id' => $orderLine->id, + 'orderId' => $orderLine->orderId, + 'name' => $orderLine->name, + 'sku' => $orderLine->sku, + 'type' => $orderLine->type, + 'status' => $orderLine->status, + 'quantity' => $orderLine->quantity, + 'unitPrice' => (array)$orderLine->unitPrice, + 'vatRate' => $orderLine->vatRate, + 'vatAmount' => (array)$orderLine->vatAmount, + 'totalAmount' => (array)$orderLine->totalAmount, + 'createdAt' => $orderLine->createdAt + ]; } - return $this->json($status); + return $this->json([ + 'id' => $shipment->id, + 'orderId' => $shipment->orderId, + 'createdAt' => $shipment->createdAt, + 'lines' => $lines, + 'tracking' => $shipment->tracking + ]); } /** - * @Route("/api/_action/mollie/ship/total", name="api.action.mollie.ship.total", methods={"POST"}) - * - * @param RequestDataBag $data - * @param Context $context + * @param Exception $e + * @param array $additionalData * @return JsonResponse */ - public function total(RequestDataBag $data, Context $context): JsonResponse + private function exceptionToJson(Exception $e, array $additionalData = []): JsonResponse { - return $this->getTotalResponse($data->get('orderId'), $context); - } + $this->logger->error( + $e->getMessage(), + $additionalData + ); - /** - * @Route("/api/v{version}/_action/mollie/ship/total", name="api.action.mollie.ship.total.legacy", methods={"POST"}) - * - * @param RequestDataBag $data - * @param Context $context - * @return JsonResponse - */ - public function totalLegacy(RequestDataBag $data, Context $context): JsonResponse - { - return $this->getTotalResponse($data->get('orderId'), $context); + return $this->json([ + 'error' => get_class($e), + 'message' => $e->getMessage(), + 'data' => $additionalData + ], 400); } /** - * @param string $orderId - * @param Context $context - * @return JsonResponse + * @param array $items + * @return ShipmentLineItem[] */ - public function getTotalResponse(string $orderId, Context $context): JsonResponse + private function hydrateShippingItems(array $items): array { - try { - $totals = $this->shipmentFacade->getTotals($orderId, $context); - } catch (ShopwareHttpException $e) { - $this->logger->error($e->getMessage()); - return $this->json(['message' => $e->getMessage()], $e->getStatusCode()); - } catch (\Throwable $e) { - $this->logger->error($e->getMessage()); - return $this->json(['message' => $e->getMessage()], 500); + $finalList = []; + + foreach ($items as $item) { + $finalList[] = new ShipmentLineItem($item['id'], $item['quantity']); } - return $this->json($totals); + return $finalList; } } diff --git a/src/Controller/Api/PluginConfig/ConfigControllerBase.php b/src/Controller/Api/PluginConfig/ConfigControllerBase.php index 09dea2bd5..88a1a5a9f 100644 --- a/src/Controller/Api/PluginConfig/ConfigControllerBase.php +++ b/src/Controller/Api/PluginConfig/ConfigControllerBase.php @@ -3,19 +3,11 @@ namespace Kiener\MolliePayments\Controller\Api\PluginConfig; use Exception; -use Kiener\MolliePayments\Facade\MollieShipment; -use Kiener\MolliePayments\Service\ConfigService; use Kiener\MolliePayments\Service\MollieApi\ApiKeyValidator; use Kiener\MolliePayments\Service\SettingsService; use Kiener\MolliePayments\Setting\MollieSettingStruct; -use Mollie\Api\MollieApiClient; -use Mollie\Api\Resources\Profile; use Shopware\Administration\Snippet\SnippetFinderInterface; -use Shopware\Core\Framework\Api\Context\AdminApiSource; -use Shopware\Core\Framework\Api\Context\Exception\InvalidContextSourceException; use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; -use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Controller/Api/PluginConfig/Sw6/ConfigController.php b/src/Controller/Api/PluginConfig/Sw6/ConfigController.php index 51b78619c..fe2ec2cfe 100644 --- a/src/Controller/Api/PluginConfig/Sw6/ConfigController.php +++ b/src/Controller/Api/PluginConfig/Sw6/ConfigController.php @@ -2,24 +2,8 @@ namespace Kiener\MolliePayments\Controller\Api\PluginConfig\Sw6; -use Exception; use Kiener\MolliePayments\Controller\Api\PluginConfig\ConfigControllerBase; -use Kiener\MolliePayments\Facade\MollieShipment; -use Kiener\MolliePayments\Service\ConfigService; -use Kiener\MolliePayments\Service\MollieApi\ApiKeyValidator; -use Kiener\MolliePayments\Service\SettingsService; -use Kiener\MolliePayments\Setting\MollieSettingStruct; -use Mollie\Api\MollieApiClient; -use Mollie\Api\Resources\Profile; -use Shopware\Administration\Snippet\SnippetFinderInterface; -use Shopware\Core\Framework\Api\Context\AdminApiSource; -use Shopware\Core\Framework\Api\Context\Exception\InvalidContextSourceException; -use Shopware\Core\Framework\Context; use Shopware\Core\Framework\Routing\Annotation\RouteScope; -use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; /** diff --git a/src/Controller/Api/PluginConfig/Sw65/ConfigController.php b/src/Controller/Api/PluginConfig/Sw65/ConfigController.php index c1e6f8c83..f9201a373 100644 --- a/src/Controller/Api/PluginConfig/Sw65/ConfigController.php +++ b/src/Controller/Api/PluginConfig/Sw65/ConfigController.php @@ -2,24 +2,7 @@ namespace Kiener\MolliePayments\Controller\Api\PluginConfig\Sw65; -use Exception; use Kiener\MolliePayments\Controller\Api\PluginConfig\ConfigControllerBase; -use Kiener\MolliePayments\Facade\MollieShipment; -use Kiener\MolliePayments\Service\ConfigService; -use Kiener\MolliePayments\Service\MollieApi\ApiKeyValidator; -use Kiener\MolliePayments\Service\SettingsService; -use Kiener\MolliePayments\Setting\MollieSettingStruct; -use Mollie\Api\MollieApiClient; -use Mollie\Api\Resources\Profile; -use Shopware\Administration\Snippet\SnippetFinderInterface; -use Shopware\Core\Framework\Api\Context\AdminApiSource; -use Shopware\Core\Framework\Api\Context\Exception\InvalidContextSourceException; -use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; -use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; /** diff --git a/src/Exception/OrderLineItemNotFoundException.php b/src/Exception/OrderLineItemNotFoundException.php index 18c982d56..2228a2225 100644 --- a/src/Exception/OrderLineItemNotFoundException.php +++ b/src/Exception/OrderLineItemNotFoundException.php @@ -14,7 +14,12 @@ class OrderLineItemNotFoundException extends ShopwareHttpException */ public function __construct(string $identifier, array $parameters = [], \Throwable $previous = null) { - $message = sprintf('Order lineitem with identifier %s could not be found', $identifier); + if (empty($identifier)) { + $message = 'Could not find an OrderLineItem. No identifier/productNumber provided'; + } else { + $message = sprintf('OrderLineItem with identifier: "%s" could not be found', $identifier); + } + parent::__construct($message, $parameters, $previous); } diff --git a/src/Facade/MollieShipment.php b/src/Facade/MollieShipment.php deleted file mode 100644 index 1021bf181..000000000 --- a/src/Facade/MollieShipment.php +++ /dev/null @@ -1,477 +0,0 @@ -extractor = $extractor; - $this->deliveryTransitionService = $deliveryTransitionService; - $this->mollieApiOrderService = $mollieApiOrderService; - $this->mollieApiShipmentService = $mollieApiShipmentService; - $this->orderDeliveryService = $orderDeliveryService; - $this->orderService = $orderService; - $this->orderDataExtractor = $orderDataExtractor; - $this->logger = $logger; - $this->trackingInfoStructFactory = $trackingInfoStructFactory; - } - - /** - * TODO: this is here just for now, because I cannot change it all right now, but we need to make sure someone can verify if the shipment should even be triggered to avoid logs being written when shipping other PSP orders - * - * @param string $orderDeliveryId - * @param Context $context - * @return null|OrderEntity - */ - public function isMollieOrder(string $orderDeliveryId, Context $context): ?OrderEntity - { - $delivery = $this->orderDeliveryService->getDelivery($orderDeliveryId, $context); - - if (!$delivery instanceof OrderDeliveryEntity) { - return null; - } - - $order = $delivery->getOrder(); - - if (!$order instanceof OrderEntity) { - return null; - } - - $lastTransaction = $this->extractor->extractLastMolliePayment($order->getTransactions()); - - if (!$lastTransaction instanceof OrderTransactionEntity) { - return null; - } - - return $order; - } - - /** - * @param string $orderDeliveryId - * @param Context $context - * @return bool - */ - public function setShipment(string $orderDeliveryId, Context $context): bool - { - $delivery = $this->orderDeliveryService->getDelivery($orderDeliveryId, $context); - - if (!$delivery instanceof OrderDeliveryEntity) { - $this->logger->warning( - sprintf('Order delivery with id %s could not be found in database', $orderDeliveryId) - ); - - return false; - } - - $order = $delivery->getOrder(); - - if (!$order instanceof OrderEntity) { - $this->logger->warning( - sprintf('Loaded delivery with id %s does not have an order in database', $orderDeliveryId) - ); - - return false; - } - - $customFields = $order->getCustomFields(); - $mollieOrderId = $customFields[CustomFieldsInterface::MOLLIE_KEY][CustomFieldsInterface::ORDER_KEY] ?? null; - - if (!$mollieOrderId) { - $this->logger->warning( - sprintf('Mollie orderId does not exist in shopware order (%s)', (string)$order->getOrderNumber()) - ); - - return false; - } - - // get last transaction if it is a mollie transaction - $lastTransaction = $this->extractor->extractLastMolliePayment($order->getTransactions()); - - if (!$lastTransaction instanceof OrderTransactionEntity) { - $this->logger->info( - sprintf( - 'The last transaction of the order (%s) is not a mollie payment! No shipment will be sent to mollie', - (string)$order->getOrderNumber() - ) - ); - - return false; - } - - $trackingInfoStruct = $this->trackingInfoStructFactory->createFromDelivery($delivery); - - $addedMollieShipment = $this->mollieApiOrderService->setShipment($mollieOrderId, $trackingInfoStruct, $order->getSalesChannelId()); - - if ($addedMollieShipment) { - $values = [CustomFieldsInterface::DELIVERY_SHIPPED => true]; - $this->orderDeliveryService->updateCustomFields($delivery, $values, $context); - } - - return $addedMollieShipment; - } - - /** - * @param string $orderId - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipOrderByOrderId( - string $orderId, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $order = $this->orderService->getOrder($orderId, $context); - return $this->shipOrder( - $order, - $trackingCarrier, - $trackingCode, - $trackingUrl, - $context - ); - } - - /** - * @param string $orderNumber - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipOrderByOrderNumber( - string $orderNumber, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $order = $this->orderService->getOrderByNumber($orderNumber, $context); - return $this->shipOrder( - $order, - $trackingCarrier, - $trackingCode, - $trackingUrl, - $context - ); - } - - /** - * @param OrderEntity $order - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipOrder( - OrderEntity $order, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $mollieOrderId = $this->orderService->getMollieOrderId($order); - - $shipment = $this->mollieApiShipmentService->shipOrder( - $mollieOrderId, - $order->getSalesChannelId(), - $this->trackingInfoStructFactory->create($trackingCarrier, $trackingCode, $trackingUrl) - ); - - $delivery = $this->orderDataExtractor->extractDelivery($order, $context); - - $this->deliveryTransitionService->shipDelivery($delivery, $context); - - return $shipment; - } - - /** - * @param string $orderId - * @param string $itemIdentifier - * @param int $quantity - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipItemByOrderId( - string $orderId, - string $itemIdentifier, - int $quantity, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $order = $this->orderService->getOrder($orderId, $context); - return $this->shipItem( - $order, - $itemIdentifier, - $quantity, - $trackingCarrier, - $trackingCode, - $trackingUrl, - $context - ); - } - - /** - * @param string $orderNumber - * @param string $itemIdentifier - * @param int $quantity - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipItemByOrderNumber( - string $orderNumber, - string $itemIdentifier, - int $quantity, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $order = $this->orderService->getOrderByNumber($orderNumber, $context); - return $this->shipItem( - $order, - $itemIdentifier, - $quantity, - $trackingCarrier, - $trackingCode, - $trackingUrl, - $context - ); - } - - /** - * @param OrderEntity $order - * @param string $itemIdentifier - * @param int $quantity - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return \Mollie\Api\Resources\Shipment - */ - public function shipItem( - OrderEntity $order, - string $itemIdentifier, - int $quantity, - string $trackingCarrier, - string $trackingCode, - string $trackingUrl, - Context $context - ): \Mollie\Api\Resources\Shipment { - $mollieOrderId = $this->orderService->getMollieOrderId($order); - - $lineItems = $this->findMatchingLineItems($order, $itemIdentifier, $context); - - if ($lineItems->count() > 1) { - throw new OrderLineItemFoundManyException($itemIdentifier); - } - - $lineItem = $lineItems->first(); - unset($lineItems); - - if (!$lineItem instanceof OrderLineItemEntity) { - throw new OrderLineItemNotFoundException($itemIdentifier); - } - - $mollieOrderLineId = $this->orderService->getMollieOrderLineId($lineItem); - - if ($quantity === 0) { - $quantity = $this->mollieApiOrderService->getMollieOrderLine( - $mollieOrderId, - $mollieOrderLineId, - $order->getSalesChannelId() - )->shippableQuantity; - } - - $shipment = $this->mollieApiShipmentService->shipItem( - $mollieOrderId, - $order->getSalesChannelId(), - $mollieOrderLineId, - $quantity, - $this->trackingInfoStructFactory->create($trackingCarrier, $trackingCode, $trackingUrl) - ); - - $delivery = $this->orderDataExtractor->extractDelivery($order, $context); - - if ($this->mollieApiOrderService->isCompletelyShipped($mollieOrderId, $order->getSalesChannelId())) { - $this->deliveryTransitionService->shipDelivery($delivery, $context); - } else { - $this->deliveryTransitionService->partialShipDelivery($delivery, $context); - } - - return $shipment; - } - - /** - * @param string $orderId - * @param Context $context - * @return array - */ - public function getStatus(string $orderId, Context $context): array - { - $order = $this->orderService->getOrder($orderId, $context); - $mollieOrderId = $this->orderService->getMollieOrderId($order); - - return $this->mollieApiShipmentService->getStatus($mollieOrderId, $order->getSalesChannelId()); - } - - /** - * @param string $orderId - * @param Context $context - * @return array - */ - public function getTotals(string $orderId, Context $context): array - { - $order = $this->orderService->getOrder($orderId, $context); - $mollieOrderId = $this->orderService->getMollieOrderId($order); - - return $this->mollieApiShipmentService->getTotals($mollieOrderId, $order->getSalesChannelId()); - } - - /** - * Try to find lineItems matching the $itemIdentifier. Shopware does not have a unique human-readable identifier for - * order line items, so we have to check for several fields, like product number or the mollie order line id. - * - * @param OrderEntity $order - * @param string $itemIdentifier - * @param Context $context - * @return OrderLineItemCollection - */ - private function findMatchingLineItems(OrderEntity $order, string $itemIdentifier, Context $context): OrderLineItemCollection - { - return $this->orderDataExtractor->extractLineItems($order, $context)->filter(function ($lineItem) use ($itemIdentifier) { - /** @var OrderLineItemEntity $lineItem */ - - // Default Shopware: If the lineItem is of type "product" and has an associated ProductEntity, - // check if the itemIdentifier matches the product's product number. - if ($lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE && - $lineItem->getProduct() instanceof ProductEntity && - $lineItem->getProduct()->getProductNumber() === $itemIdentifier) { - return true; - } - - // If it's not a "product" type lineItem, for example if it's a completely custom lineItem type, - // check if the payload has a productNumber in it that matches the itemIdentifier. - if (!empty($lineItem->getPayload()) && - array_key_exists('productNumber', $lineItem->getPayload()) && - $lineItem->getPayload()['productNumber'] === $itemIdentifier) { - return true; - } - - // Check itemIdentifier against the mollie order_line_id custom field - $customFields = $lineItem->getCustomFields() ?? []; - $mollieOrderLineId = $customFields[CustomFieldsInterface::MOLLIE_KEY]['order_line_id'] ?? null; - if (!is_null($mollieOrderLineId) && $mollieOrderLineId === $itemIdentifier) { - return true; - } - - // If it hasn't passed any of the above tests, check if the itemIdentifier is a valid Uuid... - if (!Uuid::isValid($itemIdentifier)) { - return false; - } - - // ... and then check if it matches the Id of the entity the lineItem is referencing, - // or if it matches the Id of the lineItem itself. - if ($lineItem->getReferencedId() === $itemIdentifier || $lineItem->getId() === $itemIdentifier) { - return true; - } - - // Otherwise, this lineItem does not match the itemIdentifier at all. - return false; - }); - } -} diff --git a/src/Facade/MollieShipmentInterface.php b/src/Facade/MollieShipmentInterface.php deleted file mode 100644 index 60a945e96..000000000 --- a/src/Facade/MollieShipmentInterface.php +++ /dev/null @@ -1,82 +0,0 @@ -deliveryService = $deliveryService; - $this->stateMachineRegistry = $stateMachineRegistry; - } - - /** - * Processes the order status of Mollie, if the order at Mollie is shipping, - * also synchronise it to Shopware. - * - * @param OrderEntity $order - * @param Order $mollieOrder - * @param Context $context - * @throws InconsistentCriteriaIdsException - */ - public function shipDelivery( - OrderEntity $order, - Order $mollieOrder, - Context $context - ): void { - /** @var OrderDeliveryEntity $orderDelivery */ - $orderDelivery = $this->deliveryService - ->getDeliveryByOrderId($order->getId(), $order->getVersionId()); - - /** - * Order is shipping. - */ - if ( - $orderDelivery !== null - && $mollieOrder->isShipping() - && ( - !isset($orderDelivery->getCustomFields()[self::PARAM_MOLLIE_PAYMENTS][self::PARAM_IS_SHIPPED]) - || $orderDelivery->getCustomFields()[self::PARAM_MOLLIE_PAYMENTS][self::PARAM_IS_SHIPPED] === false - ) - && ( - $orderDelivery->getStateMachineState() === null - || ( - $orderDelivery->getStateMachineState()->getTechnicalName() !== OrderDeliveryStates::STATE_SHIPPED - && $orderDelivery->getStateMachineState()->getTechnicalName() !== OrderDeliveryStates::STATE_PARTIALLY_SHIPPED - ) - ) - ) { - $transitionName = 'ship_partially'; - - if ($this->isOrderShipped($mollieOrder)) { - $transitionName = 'ship'; - } - - // Transition the order to being shipped - $this->stateMachineRegistry->transition( - new Transition( - 'order_delivery', - $orderDelivery->getId(), - $transitionName, - 'stateId' - ), - $context - ); - - // Add is shipped flag to custom fields - if ($transitionName === 'ship') { - $customFields = $order->getCustomFields() ?? []; - - $this->deliveryService->updateDelivery([ - self::PARAM_ID => $orderDelivery->getId(), - self::PARAM_CUSTOM_FIELDS => $this->deliveryService->addShippedToCustomFields($customFields, true), - ], $context); - } - } - } - - /** - * Returns whether the order is partially shipping. - * - * @param Order $order - * - * @return bool - */ - private function isOrderShipped(Order $order): bool - { - $linesQuantity = 0; - $shipmentsQuantity = 0; - - if ($order->lines()->count()) { - /** @var OrderLine $line */ - foreach ($order->lines() as $line) { - $linesQuantity += $line->quantity; - } - } - - if ($order->shipments()->count()) { - /** @var Shipment $shipment */ - foreach ($order->shipments() as $shipment) { - if ($shipment->lines()->count()) { - /** @var OrderLine $line */ - foreach ($shipment->lines() as $line) { - $shipmentsQuantity += $line->quantity; - } - } - } - } - - return ($shipmentsQuantity > 0 && $linesQuantity === $shipmentsQuantity); - } -} diff --git a/src/Resources/app/administration/src/core/service/api/mollie-payments-shipping.service.js b/src/Resources/app/administration/src/core/service/api/mollie-payments-shipping.service.js index 80cb01f8c..f933fc85a 100644 --- a/src/Resources/app/administration/src/core/service/api/mollie-payments-shipping.service.js +++ b/src/Resources/app/administration/src/core/service/api/mollie-payments-shipping.service.js @@ -2,33 +2,44 @@ const ApiService = Shopware.Classes.ApiService; class MolliePaymentsShippingService extends ApiService { + + /** + * + * @param httpClient + * @param loginService + * @param apiEndpoint + */ constructor(httpClient, loginService, apiEndpoint = 'mollie') { super(httpClient, loginService, apiEndpoint); } - __post(endpoint = '', data = {}, headers = {}) { - return this.httpClient - .post( - `_action/${this.getApiBasePath()}/ship${endpoint}`, - JSON.stringify(data), - { - headers: this.getBasicHeaders(headers), - } - ) - .then((response) => { - return ApiService.handleResponse(response); - }); - } + /** + * + * @param orderId + * @param trackingCarrier + * @param trackingCode + * @param trackingUrl + * @param items + * @returns {*} + */ + shipOrder(orderId, trackingCarrier, trackingCode, trackingUrl, items) { + + const data = { + orderId: orderId, + trackingCarrier: trackingCarrier, + trackingCode: trackingCode, + trackingUrl: trackingUrl, + items: items, + } - shipOrder(data = { - orderId: null, - trackingCarrier: null, - trackingCode: null, - trackingUrl: null, - }) { return this.__post('', data); } + /** + * + * @param data + * @returns {*} + */ shipItem(data = { orderId: null, itemId: null, @@ -40,13 +51,46 @@ class MolliePaymentsShippingService extends ApiService { return this.__post('/item', data); } + /** + * + * @param data + * @returns {*} + */ status(data = {orderId: null}) { return this.__post('/status', data); } + /** + * + * @param data + * @returns {*} + */ total(data = {orderId: null}) { return this.__post('/total', data); } + + /** + * + * @param endpoint + * @param data + * @param headers + * @returns {*} + * @private + */ + __post(endpoint = '', data = {}, headers = {}) { + return this.httpClient + .post( + `_action/${this.getApiBasePath()}/ship${endpoint}`, + JSON.stringify(data), + { + headers: this.getBasicHeaders(headers), + } + ) + .then((response) => { + return ApiService.handleResponse(response); + }); + } + } export default MolliePaymentsShippingService; diff --git a/src/Resources/app/administration/src/core/service/utils/array-utils.service.js b/src/Resources/app/administration/src/core/service/utils/array-utils.service.js new file mode 100644 index 000000000..8fbdbadfc --- /dev/null +++ b/src/Resources/app/administration/src/core/service/utils/array-utils.service.js @@ -0,0 +1,39 @@ +export default class ArrayUtilsService { + + /** + * + * @param array + * @param item + * @param key + */ + addUniqueItem(array, item, key) { + + const identifier = item[key]; + + // check if we already have this item + for (let i = 0; i < array.length; i++) { + const existingItem = array[i]; + if (existingItem[key] === identifier) { + return 2; + } + } + + array.push(item); + } + + /** + * + * @param array + * @param item + * @param key + */ + removeItem(array, item, key) { + for (let i = 0; i < array.length; i++) { + if (array[i][key] === item[key]) { + array.splice(i, 1); + return; + } + } + } + +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/MollieShipping.js b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/MollieShipping.js index 8e0d83667..e207f586c 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/MollieShipping.js +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/MollieShipping.js @@ -61,6 +61,8 @@ export default class MollieShipping { const lineItem = order.lineItems[i]; finalItems.push({ + id: lineItem.id, + mollieId: lineItem.mollieOrderLineId, label: lineItem.label, quantity: this._shippableQuantity(lineItem), }); diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/index.js b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/index.js index aeabe1fa0..8a35748dd 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/index.js +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/index.js @@ -1,4 +1,5 @@ import template from './mollie-ship-order.html.twig'; +import './mollie-ship-order.scss'; import MollieShippingEvents from './MollieShippingEvents'; import MollieShipping from './MollieShipping'; @@ -51,12 +52,23 @@ Component.register('mollie-ship-order', { getShipOrderColumns() { return [ + { + property: 'itemselect', + label: '', + }, { property: 'label', label: this.$tc('mollie-payments.modals.shipping.order.itemHeader'), - }, { + }, + { property: 'quantity', label: this.$tc('mollie-payments.modals.shipping.order.quantityHeader'), + width: '160px', + }, + { + property: 'originalQuantity', + label: this.$tc('mollie-payments.modals.shipping.order.originalQuantityHeader'), + width: '160px', }, ]; }, @@ -93,6 +105,15 @@ Component.register('mollie-ship-order', { const shipping = new MollieShipping(this.MolliePaymentsShippingService); shipping.getShippableItems(this.order).then((items) => { + + // this is required to make sure the "select all" works + // because we need to have a default value + for (let i = 0; i < items.length; i++) { + const item = items[i]; + item.selected = false; + item.originalQuantity = item.quantity; + } + this.shippableLineItems = items; }); @@ -105,19 +126,54 @@ Component.register('mollie-ship-order', { } }, + /** + * + */ + btnSelectAllItems_Click() { + for (let i = 0; i < this.shippableLineItems.length; i++) { + const item = this.shippableLineItems[i]; + if (item.originalQuantity > 0) { + item.selected = true; + } + } + }, + + /** + * + */ + btnResetItems_Click() { + for (let i = 0; i < this.shippableLineItems.length; i++) { + const item = this.shippableLineItems[i]; + item.selected = false; + item.quantity = item.originalQuantity; + } + }, + /** * */ onShipOrder() { - const params = { - orderId: this.order.id, - trackingCarrier: this.tracking.carrier, - trackingCode: this.tracking.code, - trackingUrl: this.tracking.url, - }; + var shippingItems = []; + + for (let i = 0; i < this.shippableLineItems.length; i++) { + const item = this.shippableLineItems[i]; + + if (item.selected) { + shippingItems.push({ + 'id': item.id, + 'quantity': item.quantity, + }) + } + } - this.MolliePaymentsShippingService.shipOrder(params) + this.MolliePaymentsShippingService.shipOrder( + this.order.id, + this.tracking.carrier, + this.tracking.code, + this.tracking.url, + shippingItems, + ) .then(() => { // send global event diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/mollie-ship-order.html.twig b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/mollie-ship-order.html.twig index 344b541ab..6c841680a 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/mollie-ship-order.html.twig +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/mollie-ship-order.html.twig @@ -1,6 +1,15 @@ - +

{{ $tc('mollie-payments.modals.shipping.order.description') }}

+ + + {{ $tc('mollie-payments.modals.shipping.selectAllButton') }} + + + {{ $tc('mollie-payments.modals.shipping.resetButton') }} + + + {% block sw_order_line_items_grid_grid_mollie_ship_item_modal_items %} + + + + + {% endblock %} - - {{ $tc('mollie-payments.modals.shipping.confirmButton') }} - + +
+ + {{ $tc('mollie-payments.modals.shipping.confirmButton') }} + +
{% block sw_order_line_items_grid_grid_mollie_ship_item_modal_tracking %} diff --git a/src/Resources/app/administration/src/module/mollie-payments/extension/sw-order/view/sw-order-detail-general/sw-order-detail-general.html.twig b/src/Resources/app/administration/src/module/mollie-payments/extension/sw-order/view/sw-order-detail-general/sw-order-detail-general.html.twig index 6e6bd1879..74fe784fe 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/extension/sw-order/view/sw-order-detail-general/sw-order-detail-general.html.twig +++ b/src/Resources/app/administration/src/module/mollie-payments/extension/sw-order/view/sw-order-detail-general/sw-order-detail-general.html.twig @@ -37,7 +37,7 @@ SHOPWARE 6.5 {% block sw_order_detail_general_mollie_shipping %} diff --git a/src/Resources/app/administration/src/snippet/de-DE.json b/src/Resources/app/administration/src/snippet/de-DE.json index e3deb8fae..76b59f784 100644 --- a/src/Resources/app/administration/src/snippet/de-DE.json +++ b/src/Resources/app/administration/src/snippet/de-DE.json @@ -261,7 +261,8 @@ "order": { "description": "Die folgenden Artikelmengen werden versandt.", "itemHeader": "Artikel", - "quantityHeader": "Menge" + "quantityHeader": "Menge", + "originalQuantityHeader": "Menge (verschickbar)" }, "availableTracking": { "label": "Verfügbare Tracking-Codes", @@ -275,7 +276,9 @@ "invalid": "Bitte geben Sie sowohl Spediteur als auch Code ein" }, "confirmButton": "Bestellung versenden", - "cancelButton": "Abbrechen" + "cancelButton": "Abbrechen", + "selectAllButton": "Alle auswählen", + "resetButton": "Zurücksetzen" } }, "sw-flow": { diff --git a/src/Resources/app/administration/src/snippet/en-GB.json b/src/Resources/app/administration/src/snippet/en-GB.json index 7db602446..c8901c310 100644 --- a/src/Resources/app/administration/src/snippet/en-GB.json +++ b/src/Resources/app/administration/src/snippet/en-GB.json @@ -261,7 +261,8 @@ "order": { "description": "The following item quantities will be shipped.", "itemHeader": "Item", - "quantityHeader": "Quantity" + "quantityHeader": "Quantity", + "originalQuantityHeader": "Quantity (shippable)" }, "availableTracking": { "label": "Available tracking codes", @@ -275,7 +276,9 @@ "invalid": "Please enter both Carrier and Code" }, "confirmButton": "Ship order", - "cancelButton": "Cancel" + "cancelButton": "Cancel", + "selectAllButton": "Select all", + "resetButton": "Reset" } }, "sw-flow": { diff --git a/src/Resources/app/administration/src/snippet/nl-NL.json b/src/Resources/app/administration/src/snippet/nl-NL.json index a27cc250f..b3f8b10c0 100644 --- a/src/Resources/app/administration/src/snippet/nl-NL.json +++ b/src/Resources/app/administration/src/snippet/nl-NL.json @@ -261,7 +261,8 @@ "order": { "description": "De volgende product aantallen zullen worden verzonden.", "itemHeader": "Product", - "quantityHeader": "Aantal" + "quantityHeader": "Aantal", + "originalQuantityHeader": "Aantal (verzendbaar)" }, "availableTracking": { "label": "Beschikbare tracking codes", @@ -275,7 +276,9 @@ "invalid": "Voer zowel Carrier als Code in" }, "confirmButton": "Bestelling verzenden", - "cancelButton": "Annuleren" + "cancelButton": "Annuleren", + "selectAllButton": "Selecteer alles", + "resetButton": "Resetten" } }, diff --git a/src/Resources/app/administration/tests/core/utils/array-utils.service.spec.js b/src/Resources/app/administration/tests/core/utils/array-utils.service.spec.js new file mode 100644 index 000000000..f44e0e620 --- /dev/null +++ b/src/Resources/app/administration/tests/core/utils/array-utils.service.spec.js @@ -0,0 +1,62 @@ +import ArrayUtilsService from "../../../src/core/service/utils/array-utils.service"; + +const utils = new ArrayUtilsService(); + +test('Struct can be added', () => { + + const array = []; + + const data = { + id: 1, + name: 'test', + }; + + utils.addUniqueItem(array, data, 'id'); + + expect(array.length).toBe(1); +}); + + +test('Struct cannot be added twice', () => { + + const array = []; + + const data = { + id: 1, + name: 'test', + }; + + utils.addUniqueItem(array, data, 'id'); + utils.addUniqueItem(array, data, 'id'); + + expect(array.length).toBe(1); +}); + +test('Struct can be removed again', () => { + + const array = []; + + const data = { + id: 1, + name: 'test', + }; + + utils.addUniqueItem(array, data, 'id'); + utils.removeItem(array, data, 'id'); + + expect(array.length).toBe(0); +}); + +test('Remove on empty struct does not throw exception', () => { + + const array = []; + + const data = { + id: 1, + name: 'test', + }; + + utils.removeItem(array, data, 'id'); + + expect(array.length).toBe(0); +}); \ No newline at end of file diff --git a/src/Resources/config/compatibility/controller.xml b/src/Resources/config/compatibility/controller.xml index c07eed50c..6d89afe28 100644 --- a/src/Resources/config/compatibility/controller.xml +++ b/src/Resources/config/compatibility/controller.xml @@ -48,7 +48,8 @@ - + + diff --git a/src/Resources/config/compatibility/controller_6.5.xml b/src/Resources/config/compatibility/controller_6.5.xml index 933fd6fe1..0d524d653 100644 --- a/src/Resources/config/compatibility/controller_6.5.xml +++ b/src/Resources/config/compatibility/controller_6.5.xml @@ -48,7 +48,8 @@ - + + diff --git a/src/Resources/config/compatibility/flowbuilder/6.4.6.0.xml b/src/Resources/config/compatibility/flowbuilder/6.4.6.0.xml index ff709e35a..5a87686ef 100644 --- a/src/Resources/config/compatibility/flowbuilder/6.4.6.0.xml +++ b/src/Resources/config/compatibility/flowbuilder/6.4.6.0.xml @@ -9,7 +9,7 @@ - + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index f03bd1b2e..f426f9038 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -43,11 +43,6 @@ - - - - - diff --git a/src/Resources/config/services/facades.xml b/src/Resources/config/services/facades.xml index 941f4edef..abfa9c8d4 100644 --- a/src/Resources/config/services/facades.xml +++ b/src/Resources/config/services/facades.xml @@ -5,19 +5,18 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + - + + - - diff --git a/src/Resources/config/services/payment.xml b/src/Resources/config/services/payment.xml index 13bc5466f..bbdd973fe 100644 --- a/src/Resources/config/services/payment.xml +++ b/src/Resources/config/services/payment.xml @@ -11,7 +11,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -35,7 +35,7 @@ - + @@ -48,7 +48,7 @@ - + diff --git a/src/Resources/config/services/services.xml b/src/Resources/config/services/services.xml index 0a77f60ad..26465fa93 100644 --- a/src/Resources/config/services/services.xml +++ b/src/Resources/config/services/services.xml @@ -60,6 +60,7 @@ + @@ -87,6 +88,13 @@ + + + + + + + diff --git a/src/Resources/config/services/subscriber.xml b/src/Resources/config/services/subscriber.xml index e76ae36f7..be985f435 100644 --- a/src/Resources/config/services/subscriber.xml +++ b/src/Resources/config/services/subscriber.xml @@ -8,7 +8,9 @@ - + + + diff --git a/src/Service/MollieApi/Models/MollieShippingItem.php b/src/Service/MollieApi/Models/MollieShippingItem.php new file mode 100644 index 000000000..968a581a9 --- /dev/null +++ b/src/Service/MollieApi/Models/MollieShippingItem.php @@ -0,0 +1,43 @@ +mollieItemId = $mollieItemId; + $this->quantity = $quantity; + } + + /** + * @return string + */ + public function getMollieItemId(): string + { + return $this->mollieItemId; + } + + /** + * @return int + */ + public function getQuantity(): int + { + return $this->quantity; + } +} diff --git a/src/Service/MollieApi/OrderDataExtractor.php b/src/Service/MollieApi/OrderDataExtractor.php index 9d468f896..86d6a4d16 100644 --- a/src/Service/MollieApi/OrderDataExtractor.php +++ b/src/Service/MollieApi/OrderDataExtractor.php @@ -4,15 +4,11 @@ use Kiener\MolliePayments\Exception\OrderCurrencyNotFoundException; use Kiener\MolliePayments\Exception\OrderCustomerNotFoundException; -use Kiener\MolliePayments\Exception\OrderDeliveriesNotFoundException; -use Kiener\MolliePayments\Exception\OrderDeliveryNotFoundException; use Kiener\MolliePayments\Exception\OrderLineItemsNotFoundException; use Kiener\MolliePayments\Service\CustomerService; use Psr\Log\LoggerInterface; use Shopware\Core\Checkout\Customer\CustomerEntity; use Shopware\Core\Checkout\Order\Aggregate\OrderCustomer\OrderCustomerEntity; -use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryCollection; -use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection; use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Framework\Context; @@ -103,55 +99,4 @@ public function extractLocale(OrderEntity $orderEntity, SalesChannelContext $sal return $salesChannelLanguage->getLocale(); } - - public function extractDeliveries(OrderEntity $orderEntity, Context $context): OrderDeliveryCollection - { - $deliveries = $orderEntity->getDeliveries(); - - if (!$deliveries instanceof OrderDeliveryCollection) { - $this->logger->critical( - sprintf('Could not fetch deliveries from order with id %s', $orderEntity->getId()) - ); - - throw new OrderDeliveriesNotFoundException($orderEntity->getId()); - } - - return $deliveries; - } - - public function extractDelivery(OrderEntity $orderEntity, Context $context): OrderDeliveryEntity - { - $deliveries = $this->extractDeliveries($orderEntity, $context); - - /** - * TODO: In future Shopware versions there might be multiple deliveries. There is support for multiple deliveries - * but as of writing only one delivery is created per order, which is why we use first() here. - */ - $delivery = $deliveries->first(); - - if (!$delivery instanceof OrderDeliveryEntity) { - $this->logger->critical( - sprintf('Could not fetch deliveries from order with id %s', $orderEntity->getId()) - ); - - throw new OrderDeliveryNotFoundException($orderEntity->getId()); - } - - return $delivery; - } - - public function extractLineItems(OrderEntity $orderEntity, Context $context): OrderLineItemCollection - { - $lineItems = $orderEntity->getLineItems(); - - if (!$lineItems instanceof OrderLineItemCollection) { - $this->logger->critical( - sprintf('Could not fetch line items from order with id %s', $orderEntity->getId()) - ); - - throw new OrderLineItemsNotFoundException($orderEntity->getId()); - } - - return $lineItems; - } } diff --git a/src/Service/MollieApi/OrderDeliveryExtractor.php b/src/Service/MollieApi/OrderDeliveryExtractor.php new file mode 100644 index 000000000..341157da4 --- /dev/null +++ b/src/Service/MollieApi/OrderDeliveryExtractor.php @@ -0,0 +1,65 @@ +logger = $loggerService; + } + + public function extractDeliveries(OrderEntity $orderEntity, Context $context): OrderDeliveryCollection + { + $deliveries = $orderEntity->getDeliveries(); + + if (!$deliveries instanceof OrderDeliveryCollection) { + $this->logger->critical( + sprintf('Could not fetch deliveries from order with id %s', $orderEntity->getId()) + ); + + throw new OrderDeliveriesNotFoundException($orderEntity->getId()); + } + + return $deliveries; + } + + public function extractDelivery(OrderEntity $orderEntity, Context $context): OrderDeliveryEntity + { + $deliveries = $this->extractDeliveries($orderEntity, $context); + + /** + * TODO: In future Shopware versions there might be multiple deliveries. There is support for multiple deliveries + * but as of writing only one delivery is created per order, which is why we use first() here. + */ + $delivery = $deliveries->first(); + + if (!$delivery instanceof OrderDeliveryEntity) { + $this->logger->critical( + sprintf('Could not fetch deliveries from order with id %s', $orderEntity->getId()) + ); + + throw new OrderDeliveryNotFoundException($orderEntity->getId()); + } + + return $delivery; + } +} diff --git a/src/Service/MollieApi/OrderItemsExtractor.php b/src/Service/MollieApi/OrderItemsExtractor.php new file mode 100644 index 000000000..df2a97069 --- /dev/null +++ b/src/Service/MollieApi/OrderItemsExtractor.php @@ -0,0 +1,37 @@ +getLineItems(); + + if (!$lineItems instanceof OrderLineItemCollection) { + throw new OrderLineItemsNotFoundException($orderEntity->getId()); + } + + return $lineItems; + } +} diff --git a/src/Service/MollieApi/Shipment.php b/src/Service/MollieApi/Shipment.php index a18cda11c..c838d53a6 100644 --- a/src/Service/MollieApi/Shipment.php +++ b/src/Service/MollieApi/Shipment.php @@ -3,6 +3,7 @@ namespace Kiener\MolliePayments\Service\MollieApi; use Kiener\MolliePayments\Exception\MollieOrderCouldNotBeShippedException; +use Kiener\MolliePayments\Service\MollieApi\Models\MollieShippingItem; use Kiener\MolliePayments\Struct\MollieApi\ShipmentTrackingInfoStruct; use Mollie\Api\Exceptions\ApiException; use Mollie\Api\Resources\OrderLine; @@ -10,7 +11,7 @@ use Mollie\Api\Resources\ShipmentCollection; use Mollie\Api\Types\OrderLineType; -class Shipment +class Shipment implements ShipmentInterface { /** * @var Order @@ -40,22 +41,38 @@ public function getShipments(string $mollieOrderId, string $salesChannelId): Shi /** * @param string $mollieOrderId * @param string $salesChannelId + * @param MollieShippingItem[] $items * @param null|ShipmentTrackingInfoStruct $tracking + * @throws \Exception * @return MollieShipment */ - public function shipOrder( - string $mollieOrderId, - string $salesChannelId, - ?ShipmentTrackingInfoStruct $tracking = null - ): MollieShipment { + public function shipOrder(string $mollieOrderId, string $salesChannelId, array $items, ?ShipmentTrackingInfoStruct $tracking): MollieShipment + { try { $options = []; + if ($tracking instanceof ShipmentTrackingInfoStruct) { $options['tracking'] = $tracking->toArray(); } $mollieOrder = $this->orderApiService->getMollieOrder($mollieOrderId, $salesChannelId); - return $mollieOrder->shipAll($options); + + # if we have no items + # then simply ship all + if (empty($items)) { + return $mollieOrder->shipAll($options); + } + + # if we have provided items, + # we need to build the structure first + foreach ($items as $item) { + $options['lines'][] = [ + 'id' => $item->getMollieItemId(), + 'quantity' => $item->getQuantity(), + ]; + } + + return $mollieOrder->createShipment($options); } catch (ApiException $e) { throw new MollieOrderCouldNotBeShippedException( $mollieOrderId, @@ -75,13 +92,8 @@ public function shipOrder( * @param null|ShipmentTrackingInfoStruct $tracking * @return MollieShipment */ - public function shipItem( - string $mollieOrderId, - string $salesChannelId, - string $mollieOrderLineId, - int $quantity, - ?ShipmentTrackingInfoStruct $tracking = null - ): MollieShipment { + public function shipItem(string $mollieOrderId, string $salesChannelId, string $mollieOrderLineId, int $quantity, ?ShipmentTrackingInfoStruct $tracking): MollieShipment + { try { $options = [ 'lines' => [ @@ -97,6 +109,7 @@ public function shipItem( } $mollieOrder = $this->orderApiService->getMollieOrder($mollieOrderId, $salesChannelId); + return $mollieOrder->createShipment($options); } catch (ApiException $e) { throw new MollieOrderCouldNotBeShippedException( diff --git a/src/Service/MollieApi/ShipmentInterface.php b/src/Service/MollieApi/ShipmentInterface.php new file mode 100644 index 000000000..7d2e512d9 --- /dev/null +++ b/src/Service/MollieApi/ShipmentInterface.php @@ -0,0 +1,52 @@ + + */ + public function getTotals(string $mollieOrderId, string $salesChannelId): array; + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @return array + */ + public function getStatus(string $mollieOrderId, string $salesChannelId): array; + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @return ShipmentCollection + */ + public function getShipments(string $mollieOrderId, string $salesChannelId): ShipmentCollection; + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @param MollieShippingItem[] $items + * @param null|ShipmentTrackingInfoStruct $tracking + * @return MollieShipment + */ + public function shipOrder(string $mollieOrderId, string $salesChannelId, array $items, ?ShipmentTrackingInfoStruct $tracking): MollieShipment; + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @param string $mollieOrderLineId + * @param int $quantity + * @param null|ShipmentTrackingInfoStruct $tracking + * @return MollieShipment + */ + public function shipItem(string $mollieOrderId, string $salesChannelId, string $mollieOrderLineId, int $quantity, ?ShipmentTrackingInfoStruct $tracking): MollieShipment; +} diff --git a/src/Service/OrderService.php b/src/Service/OrderService.php index 46def6f4a..5b3861eaf 100644 --- a/src/Service/OrderService.php +++ b/src/Service/OrderService.php @@ -16,6 +16,7 @@ use Mollie\Api\Types\PaymentMethod; use Psr\Log\LoggerInterface; use Shopware\Core\Checkout\Cart\Exception\OrderNotFoundException; +use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity; use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Checkout\Order\SalesChannel\OrderService as ShopwareOrderService; @@ -52,27 +53,33 @@ class OrderService implements OrderServiceInterface */ private $updateOrderTransactionCustomFields; + /** + * @var OrderDeliveryService + */ + private $orderDeliveryService; + /** * @var LoggerInterface */ protected $logger; - /** * @param OrderRepositoryInterface $orderRepository * @param ShopwareOrderService $swOrderService * @param Order $mollieOrderService - * @param UpdateOrderCustomFields $customFieldsUpdater - * @param UpdateOrderTransactionCustomFields $orderTransactionCustomFields + * @param UpdateOrderCustomFields $updateOrderCustomFields + * @param UpdateOrderTransactionCustomFields $updateOrderTransactionCustomFields + * @param OrderDeliveryService $orderDeliveryService * @param LoggerInterface $logger */ - public function __construct(OrderRepositoryInterface $orderRepository, ShopwareOrderService $swOrderService, Order $mollieOrderService, UpdateOrderCustomFields $customFieldsUpdater, UpdateOrderTransactionCustomFields $orderTransactionCustomFields, LoggerInterface $logger) + public function __construct(OrderRepositoryInterface $orderRepository, ShopwareOrderService $swOrderService, Order $mollieOrderService, UpdateOrderCustomFields $updateOrderCustomFields, UpdateOrderTransactionCustomFields $updateOrderTransactionCustomFields, OrderDeliveryService $orderDeliveryService, LoggerInterface $logger) { $this->orderRepository = $orderRepository; $this->swOrderService = $swOrderService; $this->mollieOrderService = $mollieOrderService; - $this->updateOrderCustomFields = $customFieldsUpdater; - $this->updateOrderTransactionCustomFields = $orderTransactionCustomFields; + $this->updateOrderCustomFields = $updateOrderCustomFields; + $this->updateOrderTransactionCustomFields = $updateOrderTransactionCustomFields; + $this->orderDeliveryService = $orderDeliveryService; $this->logger = $logger; } @@ -104,7 +111,7 @@ public function getOrder(string $orderId, Context $context): OrderEntity $criteria->addAssociation('transactions.paymentMethod'); $criteria->addAssociation('transactions.paymentMethod.appPaymentMethod.app'); $criteria->addAssociation('transactions.stateMachineState'); - $criteria->addAssociation(OrderExtension::REFUND_PROPERTY_NAME.'.refundItems'); # for refund manager + $criteria->addAssociation(OrderExtension::REFUND_PROPERTY_NAME . '.refundItems'); # for refund manager $order = $this->orderRepository->search($criteria, $context)->first(); @@ -143,6 +150,29 @@ public function getOrderByNumber(string $orderNumber, Context $context): OrderEn throw new OrderNumberNotFoundException($orderNumber); } + /** + * @param string $deliveryId + * @param Context $context + * @throws \Exception + * @return OrderEntity + */ + public function getOrderByDeliveryId(string $deliveryId, Context $context): OrderEntity + { + $delivery = $this->orderDeliveryService->getDelivery($deliveryId, $context); + + if (!$delivery instanceof OrderDeliveryEntity) { + throw new \Exception('Delivery with id ' . $deliveryId . ' not found'); + } + + $order = $delivery->getOrder(); + + if (!$order instanceof OrderEntity) { + throw new \Exception('Order with id ' . $delivery->getOrderId() . ' not found'); + } + + return $order; + } + /** * @param OrderEntity $order * @throws CouldNotExtractMollieOrderIdException diff --git a/src/Service/Payment/Remover/PaymentMethodRemover.php b/src/Service/Payment/Remover/PaymentMethodRemover.php index 6bdcd3b98..7638ce681 100644 --- a/src/Service/Payment/Remover/PaymentMethodRemover.php +++ b/src/Service/Payment/Remover/PaymentMethodRemover.php @@ -6,6 +6,7 @@ use Kiener\MolliePayments\Exception\MissingRequestException; use Kiener\MolliePayments\Exception\MissingRouteException; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; +use Kiener\MolliePayments\Service\MollieApi\OrderItemsExtractor; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\SettingsService; use Kiener\MolliePayments\Struct\LineItem\LineItemAttributes; @@ -36,7 +37,6 @@ abstract class PaymentMethodRemover implements PaymentMethodRemoverInterface, Ca */ protected $requestStack; - /** * @var OrderService */ @@ -48,7 +48,7 @@ abstract class PaymentMethodRemover implements PaymentMethodRemoverInterface, Ca protected $settingsService; /** - * @var OrderDataExtractor + * @var OrderItemsExtractor */ private $orderDataExtractor; @@ -62,10 +62,10 @@ abstract class PaymentMethodRemover implements PaymentMethodRemoverInterface, Ca * @param RequestStack $requestStack * @param OrderService $orderService * @param SettingsService $settingsService - * @param OrderDataExtractor $orderDataExtractor + * @param OrderItemsExtractor $orderDataExtractor * @param LoggerInterface $logger */ - public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, OrderDataExtractor $orderDataExtractor, LoggerInterface $logger) + public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, OrderItemsExtractor $orderDataExtractor, LoggerInterface $logger) { $this->container = $container; $this->requestStack = $requestStack; @@ -237,7 +237,7 @@ protected function isSubscriptionCart(Cart $cart): bool */ protected function isSubscriptionOrder(OrderEntity $order, Context $context): bool { - $lineItems = $this->orderDataExtractor->extractLineItems($order, $context); + $lineItems = $this->orderDataExtractor->extractLineItems($order); /** @var OrderLineItemEntity $lineItem */ foreach ($lineItems as $lineItem) { @@ -258,7 +258,7 @@ protected function isSubscriptionOrder(OrderEntity $order, Context $context): bo */ protected function isVoucherOrder(OrderEntity $order, Context $context): bool { - $lineItems = $this->orderDataExtractor->extractLineItems($order, $context); + $lineItems = $this->orderDataExtractor->extractLineItems($order); /** @var OrderLineItemEntity $lineItem */ foreach ($lineItems as $lineItem) { diff --git a/src/Service/Payment/Remover/RegularPaymentRemover.php b/src/Service/Payment/Remover/RegularPaymentRemover.php index ebe445b65..40ca6088a 100644 --- a/src/Service/Payment/Remover/RegularPaymentRemover.php +++ b/src/Service/Payment/Remover/RegularPaymentRemover.php @@ -3,6 +3,7 @@ namespace Kiener\MolliePayments\Service\Payment\Remover; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; +use Kiener\MolliePayments\Service\MollieApi\OrderItemsExtractor; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\SettingsService; use Kiener\MolliePayments\Struct\PaymentMethod\PaymentMethodAttributes; @@ -20,10 +21,10 @@ class RegularPaymentRemover extends PaymentMethodRemover * @param RequestStack $requestStack * @param OrderService $orderService * @param SettingsService $settingsService - * @param OrderDataExtractor $orderDataExtractor + * @param OrderItemsExtractor $orderDataExtractor * @param LoggerInterface $logger */ - public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, OrderDataExtractor $orderDataExtractor, LoggerInterface $logger) + public function __construct(ContainerInterface $container, RequestStack $requestStack, OrderService $orderService, SettingsService $settingsService, OrderItemsExtractor $orderDataExtractor, LoggerInterface $logger) { parent::__construct($container, $requestStack, $orderService, $settingsService, $orderDataExtractor, $logger); } diff --git a/src/Service/TrackingInfoStructFactory.php b/src/Service/TrackingInfoStructFactory.php index 2a5158a58..fdd07e955 100644 --- a/src/Service/TrackingInfoStructFactory.php +++ b/src/Service/TrackingInfoStructFactory.php @@ -3,23 +3,47 @@ namespace Kiener\MolliePayments\Service; +use Kiener\MolliePayments\Components\ShipmentManager\Exceptions\NoDeliveriesFoundException; use Kiener\MolliePayments\Struct\MollieApi\ShipmentTrackingInfoStruct; +use Kiener\MolliePayments\Traits\StringTrait; +use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryCollection; use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; +use Shopware\Core\Checkout\Order\OrderEntity; class TrackingInfoStructFactory { + use StringTrait; + + /** * Mollie throws an error with length >= 100 */ const MAX_TRACKING_CODE_LENGTH = 99; - public function createFromDelivery(OrderDeliveryEntity $orderDeliveryEntity): ?ShipmentTrackingInfoStruct + + /** + * @param OrderEntity $order + * @throws NoDeliveriesFoundException + * @return null|ShipmentTrackingInfoStruct + */ + public function trackingFromOrder(OrderEntity $order): ?ShipmentTrackingInfoStruct { + # automatically extract from order + $deliveries = $order->getDeliveries(); + + if (!$deliveries instanceof OrderDeliveryCollection || $deliveries->count() === 0) { + throw new NoDeliveriesFoundException('No deliveries found for order with ID ' . $order->getId() . '!'); + } + + $orderDeliveryEntity = $deliveries->first(); + $trackingCodes = $orderDeliveryEntity->getTrackingCodes(); $shippingMethod = $orderDeliveryEntity->getShippingMethod(); + if ($shippingMethod === null) { return null; } + /** * Currently we create one shipping in mollie for one order. one shipping object can have only one tracking code. * When we have multiple Tracking Codes, we do not know which tracking code we should send to mollie. So we just dont send any tracking information at all @@ -30,14 +54,30 @@ public function createFromDelivery(OrderDeliveryEntity $orderDeliveryEntity): ?S return null; } - return $this->createInfoStruct((string)$shippingMethod->getName(), $trackingCodes[0], (string)$shippingMethod->getTrackingUrl()); + return $this->createInfoStruct( + (string)$shippingMethod->getName(), + $trackingCodes[0], + (string)$shippingMethod->getTrackingUrl() + ); } + /** + * @param string $trackingCarrier + * @param string $trackingCode + * @param string $trackingUrl + * @return null|ShipmentTrackingInfoStruct + */ public function create(string $trackingCarrier, string $trackingCode, string $trackingUrl): ?ShipmentTrackingInfoStruct { return $this->createInfoStruct($trackingCarrier, $trackingCode, $trackingUrl); } + /** + * @param string $trackingCarrier + * @param string $trackingCode + * @param string $trackingUrl + * @return null|ShipmentTrackingInfoStruct + */ private function createInfoStruct(string $trackingCarrier, string $trackingCode, string $trackingUrl): ?ShipmentTrackingInfoStruct { if (empty($trackingCarrier) && empty($trackingCode)) { @@ -68,15 +108,18 @@ private function createInfoStruct(string $trackingCarrier, string $trackingCode, } + # had the use case of this pattern, and it broke the sprintf below + if ($this->stringContains($trackingUrl, '%s%')) { + $trackingUrl = ''; + } + $trackingUrl = trim(sprintf($trackingUrl, $trackingCode)); if (filter_var($trackingUrl, FILTER_VALIDATE_URL) === false) { $trackingUrl = ''; } - /** - * following characters are not allowed in the tracking URL {,},<,>,# - */ + # following characters are not allowed in the tracking URL {,},<,>,# if (preg_match_all('/[{}<>#]/m', $trackingUrl)) { $trackingUrl = ''; } diff --git a/src/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php b/src/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php index 3968d8aae..669edbb7f 100644 --- a/src/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php +++ b/src/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php @@ -12,6 +12,10 @@ class OrderLineItemEntityAttributes */ private $item; + /** + * @var string + */ + private $productNumber; /** * @var string @@ -54,8 +58,16 @@ class OrderLineItemEntityAttributes */ public function __construct(OrderLineItemEntity $lineItem) { + $this->productNumber = ''; + $this->item = $lineItem; + $payload = $lineItem->getPayload(); + + if (is_array($payload) && array_key_exists('productNumber', $payload)) { + $this->productNumber = (string)$payload['productNumber']; + } + $this->voucherType = $this->getCustomFieldValue($lineItem, 'voucher_type'); $this->mollieOrderLineID = $this->getCustomFieldValue($lineItem, 'order_line_id'); @@ -67,6 +79,15 @@ public function __construct(OrderLineItemEntity $lineItem) $this->subscriptionRepetitionCount = (int)$this->getCustomFieldValue($lineItem, 'subscription_repetition'); } + + /** + * @return string + */ + public function getProductNumber(): string + { + return (string)$this->productNumber; + } + /** * @return string */ diff --git a/src/Subscriber/OrderDeliverySubscriber.php b/src/Subscriber/OrderDeliverySubscriber.php index 1786f0e34..f03bfde78 100644 --- a/src/Subscriber/OrderDeliverySubscriber.php +++ b/src/Subscriber/OrderDeliverySubscriber.php @@ -2,10 +2,14 @@ namespace Kiener\MolliePayments\Subscriber; -use Kiener\MolliePayments\Facade\MollieShipment; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManager; +use Kiener\MolliePayments\Repository\OrderTransaction\OrderTransactionRepositoryInterface; +use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\SettingsService; +use Kiener\MolliePayments\Struct\PaymentMethod\PaymentMethodAttributes; use Psr\Log\LoggerInterface; use Shopware\Core\Checkout\Order\OrderEntity; +use Shopware\Core\Checkout\Payment\PaymentMethodEntity; use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions; use Shopware\Core\System\StateMachine\Event\StateMachineStateChangeEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -18,28 +22,42 @@ class OrderDeliverySubscriber implements EventSubscriberInterface private $settings; /** - * @var MollieShipment + * @var ShipmentManager */ private $mollieShipment; + /** + * @var OrderService + */ + private $orderService; + + /** + * @var OrderTransactionRepositoryInterface + */ + private $repoOrderTransactions; + /** * @var LoggerInterface */ private $logger; - /** * @param SettingsService $settings - * @param MollieShipment $mollieShipment + * @param ShipmentManager $mollieShipment + * @param OrderService $orderService + * @param OrderTransactionRepositoryInterface $repoOrderTransactions * @param LoggerInterface $logger */ - public function __construct(SettingsService $settings, MollieShipment $mollieShipment, LoggerInterface $logger) + public function __construct(SettingsService $settings, ShipmentManager $mollieShipment, OrderService $orderService, OrderTransactionRepositoryInterface $repoOrderTransactions, LoggerInterface $logger) { $this->settings = $settings; $this->mollieShipment = $mollieShipment; + $this->orderService = $orderService; + $this->repoOrderTransactions = $repoOrderTransactions; $this->logger = $logger; } + /** * @return array */ @@ -75,17 +93,35 @@ public function onOrderDeliveryChanged(StateMachineStateChangeEvent $event): voi return; } - /** @var ?OrderEntity $mollieOrder */ - $mollieOrder = $this->mollieShipment->isMollieOrder($event->getTransition()->getEntityId(), $event->getContext()); + $orderDeliveryId = $event->getTransition()->getEntityId(); - # don't do anything for orders of other PSPs. - # the code below would also create logs until we refactor it, which is wrong for other PSPs - if (!$mollieOrder instanceof OrderEntity) { + try { + $order = $this->orderService->getOrderByDeliveryId($orderDeliveryId, $event->getContext()); + + $swTransaction = $this->repoOrderTransactions->getLatestOrderTransaction($order->getId(), $event->getContext()); + + # verify if the customer really paid with Mollie in the end + $paymentMethod = $swTransaction->getPaymentMethod(); + + if (!$paymentMethod instanceof PaymentMethodEntity) { + throw new \Exception('Transaction ' . $swTransaction->getId() . ' has no payment method!'); + } + + $paymentMethodAttributes = new PaymentMethodAttributes($paymentMethod); + + if (!$paymentMethodAttributes->isMolliePayment()) { + # just skip it if it has been paid + # with another payment provider + # do NOT throw an error + return; + } + + $this->logger->info('Starting Shipment through Order Delivery Transition for order: ' . $order->getOrderNumber()); + + $this->mollieShipment->shipOrderRest($order, null, $event->getContext()); + } catch (\Throwable $ex) { + # do nothing in error in subscriber return; } - - $this->logger->info('Starting Shipment through Order Delivery Transition for order: ' . $mollieOrder->getOrderNumber()); - - $this->mollieShipment->setShipment($event->getTransition()->getEntityId(), $event->getContext()); } } diff --git a/src/Traits/StringTrait.php b/src/Traits/StringTrait.php index 77113dd52..0c9430ffa 100644 --- a/src/Traits/StringTrait.php +++ b/src/Traits/StringTrait.php @@ -18,4 +18,16 @@ protected function stringStartsWith(string $haystack, string $needle): bool return false; } + + /** + * + */ + protected function stringContains(string $haystack, string $needle): bool + { + if (strpos($haystack, $needle) !== false) { + return true; + } + + return false; + } } diff --git a/tests/Cypress/cypress/e2e/storefront/shipment/shipment.cy.js b/tests/Cypress/cypress/e2e/storefront/shipment/shipment.cy.js index dfbc89c84..a7e30d72e 100644 --- a/tests/Cypress/cypress/e2e/storefront/shipment/shipment.cy.js +++ b/tests/Cypress/cypress/e2e/storefront/shipment/shipment.cy.js @@ -69,7 +69,7 @@ context("Order Shipping", () => { repoShippingFull.getFirstItemQuantity().should('contain.text', '1'); repoShippingFull.getSecondItemQuantity().should('contain.text', '1'); - shippingAction.shipOrder(); + shippingAction.shipFullOrder(); // verify delivery status and item shipped count assertShippingStatus('Shipped', 2); @@ -103,7 +103,7 @@ context("Order Shipping", () => { repoShippingFull.getTrackingCode().should('have.value', TRACKING_CODE); repoShippingFull.getTrackingUrl().should('not.have.value', ''); - shippingAction.shipOrder(); + shippingAction.shipFullOrder(); assertShippingStatus('Shipped', 2); @@ -119,7 +119,27 @@ context("Order Shipping", () => { repoOrderDetails.getMollieActionButtonShipThroughMollie().should('have.class', disabledClassName); }) - it('C4040: Partial Shipping in Administration', () => { + it('C2138608: Partial Batch Shipping in Administration', () => { + + createOrderAndOpenAdmin(2, 1); + + adminOrders.openShipThroughMollie(); + + // make sure our modal is visible + cy.contains('.sw-modal__header', 'Ship through Mollie', {timeout: 50000}); + + // verify we have 2x 1 item + // we use contain because linebreaks \n exist. + // but we don't add 11 items...so that should be fine + repoShippingFull.getFirstItemQuantity().should('contain.text', '1'); + repoShippingFull.getSecondItemQuantity().should('contain.text', '1'); + + shippingAction.shipBatchOrder(); + + assertShippingStatus('Shipped (partially)', 1); + }) + + it('C4040: Line Item Shipping in Administration', () => { createOrderAndOpenAdmin(2, 2); @@ -158,7 +178,7 @@ context("Order Shipping", () => { // so the first item is actually our second one that was not yet shipped. repoShippingFull.getFirstItemQuantity().should('contain.text', '2'); - shippingAction.shipOrder(); + shippingAction.shipFullOrder(); assertShippingStatus('Shipped', 4); @@ -175,7 +195,7 @@ context("Order Shipping", () => { repoOrderDetails.getMollieActionButtonShipThroughMollie().should('have.class', disabledClassName); }) - it('C4044: Partial Shipping with Tracking', () => { + it('C4044: Line Item Shipping with Tracking', () => { const TRACKING_CODE1 = 'code-1'; const TRACKING_CODE2 = 'code-2'; diff --git a/tests/Cypress/cypress/support/actions/admin/ShipThroughMollieAction.js b/tests/Cypress/cypress/support/actions/admin/ShipThroughMollieAction.js index fdb8d4be7..5c1b84976 100644 --- a/tests/Cypress/cypress/support/actions/admin/ShipThroughMollieAction.js +++ b/tests/Cypress/cypress/support/actions/admin/ShipThroughMollieAction.js @@ -16,10 +16,31 @@ export default class ShipThroughMollieAction { /** * */ - shipOrder() { + shipFullOrder() { cy.wait(2000); + // select all items, otherwise + // nothing would be shipped + repoShippingFull.getSelectAllItemsButton().click(); + + repoShippingFull.getShippingButton().click(forceOption); + + // here are automatic reloads and things as it seems + // I really want to test the real UX, so we just wait like a human + cy.wait(4000); + } + + /** + * + */ + shipBatchOrder() { + + cy.wait(2000); + + // select our first item + repoShippingFull.getFirstItemSelectCheckbox().click(); + repoShippingFull.getShippingButton().click(forceOption); // here are automatic reloads and things as it seems diff --git a/tests/Cypress/cypress/support/repositories/admin/ship-through-mollie/FullShippingRepository.js b/tests/Cypress/cypress/support/repositories/admin/ship-through-mollie/FullShippingRepository.js index 868a43736..b57615839 100644 --- a/tests/Cypress/cypress/support/repositories/admin/ship-through-mollie/FullShippingRepository.js +++ b/tests/Cypress/cypress/support/repositories/admin/ship-through-mollie/FullShippingRepository.js @@ -1,6 +1,22 @@ export default class FullShippingRepository { + /** + * + * @returns {Cypress.Chainable>} + */ + getSelectAllItemsButton() { + return cy.get('[style="grid-template-columns: 1fr 1fr 4fr; place-items: stretch;"] > :nth-child(1) > .sw-button__content'); + } + + /** + * + * @returns {Cypress.Chainable>} + */ + getFirstItemSelectCheckbox() { + return cy.get('.sw-data-grid__row--0 > .sw-data-grid__cell--itemselect > .sw-data-grid__cell-content'); + } + /** * * @returns {Cypress.Chainable>} diff --git a/tests/PHPUnit/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderActionTest.php b/tests/PHPUnit/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderActionTest.php index 2b7bdf392..2fa89effe 100644 --- a/tests/PHPUnit/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderActionTest.php +++ b/tests/PHPUnit/Compatibility/Bundles/FlowBuilder/Actions/ShipOrderActionTest.php @@ -6,7 +6,7 @@ use Kiener\MolliePayments\Compatibility\Bundles\FlowBuilder\Actions\ShipOrderAction; use Mollie\Api\Resources\Refund; use Mollie\Api\Types\RefundStatus; -use MolliePayments\Tests\Fakes\FakeMollieShipment; +use MolliePayments\Tests\Fakes\FakeShipmentManager; use MolliePayments\Tests\Fakes\FakeOrderService; use MolliePayments\Tests\Traits\FlowBuilderTestTrait; use PHPUnit\Framework\TestCase; @@ -50,7 +50,7 @@ public function testShippingAction() $order->setOrderNumber('ord-123'); $fakeOrderService = new FakeOrderService($order); - $fakeShipment = new FakeMollieShipment(); + $fakeShipment = new FakeShipmentManager(); $flowEvent = $this->buildOrderStateFlowEvent($order, 'action.mollie.order.ship'); diff --git a/tests/PHPUnit/Components/ShipmentManager/ShipmentManagerTest.php b/tests/PHPUnit/Components/ShipmentManager/ShipmentManagerTest.php new file mode 100644 index 000000000..d14ce1de2 --- /dev/null +++ b/tests/PHPUnit/Components/ShipmentManager/ShipmentManagerTest.php @@ -0,0 +1,484 @@ +fakeShipmentService = new FakeShipment(); + + $deliveryTransitionService = $this->createMock(DeliveryTransitionService::class); + $mollieApiOrderService = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); + $orderDeliveryService = $this->getMockBuilder(OrderDeliveryService::class)->disableOriginalConstructor()->getMock(); + $orderService = $this->getMockBuilder(OrderService::class)->disableOriginalConstructor()->getMock(); + $deliveryExtractor = new OrderDeliveryExtractor(new NullLogger()); + + $this->shipmentManager = new ShipmentManager( + $deliveryTransitionService, + $mollieApiOrderService, + $this->fakeShipmentService, + $orderDeliveryService, + $orderService, + $deliveryExtractor, + new OrderItemsExtractor(), + new TrackingInfoStructFactory() + ); + + $this->context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); + } + + + /** + * This test verifies that our shipOrderRest works correctly. + * This is defined by passing an empty line item array to our shipment service. + * We also do not provide any tracking information. In this case, the tracking data will be + * read from the order, which is also empty in this test. + * + * @return void + * @throws \Exception + */ + public function testShipOrderRestWithoutTracking(): void + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + + $this->shipmentManager->shipOrderRest($order, null, $this->context); + + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # no items should be passed on to do a "shipAll" call + $this->assertCount(0, $this->fakeShipmentService->getShippedItems()); + # no tracking data should be passed on + $this->assertNull($this->fakeShipmentService->getShippedTracking()); + } + + /** + * This test verifies that our shipOrderRest works correctly. + * This is defined by passing an empty line item array to our shipment service. + * We also do not provide any tracking information. But our order as tracking information, + * so it should be passed on correctly to Mollie. + * + * @return void + * @throws \Exception + */ + public function testShipOrderRestWithTrackingFromDelivery(): void + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + + /** @var OrderDeliveryEntity $delivery */ + $delivery = $order->getDeliveries()->first(); + $delivery->setTrackingCodes(['code-123']); + + $this->shipmentManager->shipOrderRest($order, null, $this->context); + + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # no items should be passed on to do a "shipAll" call + $this->assertCount(0, $this->fakeShipmentService->getShippedItems()); + # delivery tracking data should be passed on + $this->assertEquals('code-123', $this->fakeShipmentService->getShippedTracking()->getCode()); + } + + /** + * This test verifies that our shipOrderRest works correctly. + * This is defined by passing an empty line item array to our shipment service. + * We also provide custom tracking data that needs to be used. + * + * @return void + * @throws \Exception + */ + public function testShipOrderRestWithCustomTracking(): void + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + + $trackingData = new TrackingData( + 'DHL Standard', + 'code-abc', + 'https://www.mollie.com?code=%s' + ); + + $this->shipmentManager->shipOrderRest($order, $trackingData, $this->context); + + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # no items should be passed on to do a "shipAll" call + $this->assertCount(0, $this->fakeShipmentService->getShippedItems()); + # custom tracking data should be passed on + $this->assertEquals('code-abc', $this->fakeShipmentService->getShippedTracking()->getCode()); + $this->assertEquals('DHL Standard', $this->fakeShipmentService->getShippedTracking()->getCarrier()); + $this->assertEquals('https://www.mollie.com?code=code-abc', $this->fakeShipmentService->getShippedTracking()->getUrl()); + } + + /** + * This test verifies that we get a successful exception + * if our order in Shopware somehow has no deliveries. + * + * @return void + * @throws \Exception + */ + public function testShipOrderRestFailsWithoutDeliveries() + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1])); + + # overwrite deliveries + $order->setDeliveries(new OrderDeliveryCollection([])); + + $this->expectException(NoDeliveriesFoundException::class); + + $this->shipmentManager->shipOrderRest( + $order, + null, + $this->context + ); + + # make sure we don't call the Mollie API + $this->assertFalse($this->fakeShipmentService->isShipOrderCalled()); + } + + /** + * This test verifies that our shipOrder throws a valid exception + * if no line items have been provided. + * + * @return void + * @throws \Exception + */ + public function testShipOrderWithoutTrackingNoLineItems() + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + + $this->expectException(NoLineItemsProvidedException::class); + + $this->shipmentManager->shipOrder($order, null, [], $this->context); + + # make sure we don't call the Mollie API + $this->assertFalse($this->fakeShipmentService->isShipOrderCalled()); + } + + /** + * This test verifies that our shipOrder work correctly. + * We need to provide a line item for this. + * In this test case we do not have any tracking information, neither in the + * custom request, nor in the order delivery itself, so nothing should be tracked. + * + * @return void + * @throws \Exception + */ + public function testShipOrderWithoutTracking() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + $this->shipmentManager->shipOrder( + $order, + null, + [ + new ShipmentLineItem($lineItem1->getId(), 1), + ], + $this->context + ); + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # 1 line item should be passed + $this->assertCount(1, $this->fakeShipmentService->getShippedItems()); + # no tracking is sent + $this->assertNull($this->fakeShipmentService->getShippedTracking()); + } + + /** + * This test verifies that our shipOrder work correctly. + * We need to provide a line item for this. + * In this test case we do provide any tracking information, + * but the order already has one, so it should be used. + * + * @return void + * @throws \Exception + */ + public function testShipOrderWithTrackingFromDelivery() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + /** @var OrderDeliveryEntity $delivery */ + $delivery = $order->getDeliveries()->first(); + $delivery->setTrackingCodes(['code-123']); + + $this->shipmentManager->shipOrder( + $order, + null, + [ + new ShipmentLineItem($lineItem1->getId(), 1), + ], + $this->context + ); + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # 1 line item should be passed + $this->assertCount(1, $this->fakeShipmentService->getShippedItems()); + # delivery tracking data should be passed on + $this->assertEquals('code-123', $this->fakeShipmentService->getShippedTracking()->getCode()); + } + + /** + * This test verifies that our shipOrder work correctly. + * We need to provide a line item for this. + * In this test case we provide custom tracking information, + * which should be used. + * + * @return void + * @throws \Exception + */ + public function testShipOrderWithCustomTracking() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + $trackingData = new TrackingData( + 'DHL Standard', + 'code-abc', + 'https://www.mollie.com?code=%s' + ); + + $this->shipmentManager->shipOrder( + $order, + $trackingData, + [ + new ShipmentLineItem($lineItem1->getId(), 1), + ], + $this->context + ); + + $this->assertTrue($this->fakeShipmentService->isShipOrderCalled()); + # make sure that the correct order ID is passed on + $this->assertEquals('ord_123', $this->fakeShipmentService->getShippedMollieOrderId()); + # 1 line item should be passed + $this->assertCount(1, $this->fakeShipmentService->getShippedItems()); + # custom tracking data should be passed on + $this->assertEquals('code-abc', $this->fakeShipmentService->getShippedTracking()->getCode()); + $this->assertEquals('DHL Standard', $this->fakeShipmentService->getShippedTracking()->getCarrier()); + $this->assertEquals('https://www.mollie.com?code=code-abc', $this->fakeShipmentService->getShippedTracking()->getUrl()); + } + + /** + * This test verifies that we get a successful exception + * if our order in Shopware somehow has no deliveries. + * + * @return void + * @throws \Exception + */ + public function testShipOrderFailsWithoutDeliveries() + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1])); + + # overwrite deliveries + $order->setDeliveries(new OrderDeliveryCollection([])); + + $this->expectException(NoDeliveriesFoundException::class); + + $this->shipmentManager->shipOrder( + $order, + null, + [ + new ShipmentLineItem($lineItem1->getId(), 1) + ], + $this->context + ); + + # make sure we don't call the Mollie API + $this->assertFalse($this->fakeShipmentService->isShipOrderCalled()); + } + + /** + * This test verifies if a specific item shipment is correctly being passed on. + * We do not provide any tracking information, neither in the custom request, nor in the order delivery itself. + * + * @return void + * @throws \Exception + */ + public function testShipItemWithoutTracking() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + $this->shipmentManager->shipItem( + $order, + 'SKU-1', + 2, + null, + $this->context + ); + + # make sure that the correct order ID is passed on + $this->assertTrue($this->fakeShipmentService->isShipItemCalled()); + # 1 line item should be passed + $this->assertCount(1, $this->fakeShipmentService->getShippedItems()); + $this->assertEquals(2, $this->fakeShipmentService->getShippedItemQty()); + # no tracking is sent + $this->assertNull($this->fakeShipmentService->getShippedTracking()); + } + + /** + * This test verifies if a specific item shipment is correctly being passed on. + * We do not provide custom tracking data, but our order delivery has data which should be used. + * + * @return void + * @throws \Exception + */ + public function testShipItemWithTrackingFromDelivery() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + /** @var OrderDeliveryEntity $delivery */ + $delivery = $order->getDeliveries()->first(); + $delivery->setTrackingCodes(['code-123']); + + $this->shipmentManager->shipItem( + $order, + 'SKU-1', + 2, + null, + $this->context + ); + + # make sure that the correct order ID is passed on + $this->assertTrue($this->fakeShipmentService->isShipItemCalled()); + # delivery tracking data should be passed on + $this->assertEquals('code-123', $this->fakeShipmentService->getShippedTracking()->getCode()); + } + + /** + * This test verifies if a specific item shipment is correctly being passed on. + * We do provide custom tracking data that should be used + * + * @return void + * @throws \Exception + */ + public function testShipItemWithCustomTracking() + { + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $lineItem2 = $this->buildLineItemEntity('SKU-2'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1, $lineItem2])); + + $this->shipmentManager->shipItem( + $order, + 'SKU-1', + 2, + new TrackingData( + 'DHL Standard', + 'code-abc', + 'https://www.mollie.com?code=%s'), + $this->context + ); + + # make sure that the correct order ID is passed on + $this->assertTrue($this->fakeShipmentService->isShipItemCalled()); + # delivery tracking data should be passed on + $this->assertEquals('code-abc', $this->fakeShipmentService->getShippedTracking()->getCode()); + } + + /** + * This test verifies that we get a successful exception + * if our order in Shopware somehow has no deliveries. + * + * @return void + * @throws \Exception + */ + public function testShipItemFailsWithoutDeliveries() + { + # we build an order without a delivery that contains tracking information + $order = $this->buildMollieOrder('ord_123'); + $lineItem1 = $this->buildLineItemEntity('SKU-1'); + $order->setLineItems(new OrderLineItemCollection([$lineItem1])); + + # overwrite deliveries + $order->setDeliveries(new OrderDeliveryCollection([])); + + $this->expectException(NoDeliveriesFoundException::class); + + $this->shipmentManager->shipItem( + $order, + 'SKU-1', + 2, + null, + $this->context + ); + + # make sure we don't call the Mollie API + $this->assertFalse($this->fakeShipmentService->isShipOrderCalled()); + } + +} \ No newline at end of file diff --git a/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php b/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php deleted file mode 100644 index 49c03c301..000000000 --- a/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php +++ /dev/null @@ -1,128 +0,0 @@ -order = $this->createConfiguredMock(OrderEntity::class, [ - 'getSalesChannelId' => 'foo' - ]); - - $this->delivery = $this->createMock(OrderDeliveryEntity::class); - - $this->shipment = $this->createMock(ShipmentResource::class); - - $this->shipmentApi = $this->createConfiguredMock(Shipment::class, [ - 'shipOrder' => $this->shipment - ]); - - $this->orderService = $this->createConfiguredMock(OrderService::class, [ - 'getMollieOrderId' => 'bar' - ]); - - $this->orderDataExtractor = $this->createConfiguredMock(OrderDataExtractor::class, [ - 'extractDelivery' => $this->delivery - ]); - - - $this->shipmentFacade = new MollieShipment( - $this->createMock(MolliePaymentExtractor::class), - $this->createMock(DeliveryTransitionService::class), - $this->createMock(Order::class), - $this->shipmentApi, - $this->createMock(OrderDeliveryService::class), - $this->orderService, - $this->orderDataExtractor, - new TrackingInfoStructFactory(), - new NullLogger(), - ); - - $this->context = $this->createMock(Context::class); - } - - public function testTrackingInfoStructWithEmptyTrackingDataReturnsNull() - { - $this->shipmentApi - ->expects($this->once()) - ->method('shipOrder') - ->willReturnCallback(function ($mollieOrderId, $salesChannelId, $trackingInfoStruct) { - $this->assertNull($trackingInfoStruct); - }); - - $this->shipmentFacade->shipOrder($this->order, '', '', '', $this->context); - } - - public function testTrackingInfoStructWithMissingTrackingCarrierThrowsException() - { - $this->expectException(InvalidArgumentException::class); - - $this->shipmentFacade->shipOrder($this->order, '', '123456789', '', $this->context); - } - - public function testTrackingInfoStructWithMissingTrackingCodeThrowsException() - { - $this->expectException(InvalidArgumentException::class); - - $this->shipmentFacade->shipOrder($this->order, 'Mollie', '', '', $this->context); - } - - public function testTrackingInfoStructWithCorrectData() - { - $this->shipmentApi - ->expects($this->once()) - ->method('shipOrder') - ->willReturnCallback(function ($mollieOrderId, $salesChannelId, $trackingInfoStruct) { - $this->assertInstanceOf(ShipmentTrackingInfoStruct::class, $trackingInfoStruct); - }); - - $this->shipmentFacade->shipOrder($this->order, 'Mollie', '123456789', 'https://foo.bar?code=%s', $this->context); - } -} diff --git a/tests/PHPUnit/Facade/MollieShipment/SetShipmentTest.php b/tests/PHPUnit/Facade/MollieShipment/SetShipmentTest.php deleted file mode 100644 index bef93d763..000000000 --- a/tests/PHPUnit/Facade/MollieShipment/SetShipmentTest.php +++ /dev/null @@ -1,291 +0,0 @@ -context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); - $this->extractor = new MolliePaymentExtractor(); - $this->deliveryTransitionService = $this->createMock(DeliveryTransitionService::class); - $this->mollieApiOrderService = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); - $this->mollieApiShipmentService = $this->getMockBuilder(Shipment::class)->disableOriginalConstructor()->getMock(); - $this->orderDeliveryService = $this->getMockBuilder(OrderDeliveryService::class)->disableOriginalConstructor()->getMock(); - $this->orderService = $this->getMockBuilder(OrderService::class)->disableOriginalConstructor()->getMock(); - - $this->logger = new NullLogger(); - - $this->orderDataExtractor = new OrderDataExtractor( - $this->logger, - $this->createMock(CustomerService::class) - ); - - $trackingStructFactory = new TrackingInfoStructFactory(); - - $this->mollieShipment = new MollieShipment( - $this->extractor, - $this->deliveryTransitionService, - $this->mollieApiOrderService, - $this->mollieApiShipmentService, - $this->orderDeliveryService, - $this->orderService, - $this->orderDataExtractor, - $trackingStructFactory, - $this->logger - ); - $this->orderNumber = 'fooOrderNumber'; - } - - public function testInvalidDeliveryId(): void - { - $deliveryId = 'foo'; - $this->orderDeliveryService->method('getDelivery')->willReturn(null); - - // custom fields for shipping are never written - $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); - // api call is never done - $this->mollieApiOrderService->expects($this->never())->method('setShipment'); - // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - public function testMissingOrder(): void - { - $delivery = $this->createDelivery(null); - $deliveryId = $delivery->getId(); - $this->orderDeliveryService->method('getDelivery')->willReturn($delivery); - - // custom fields for shipping are never written - $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); - // api call is never done - $this->mollieApiOrderService->expects($this->never())->method('setShipment'); - // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - public function testMissingCustomFieldsInOrder(): void - { - $order = $this->createOrder(null); - $delivery = $this->createDelivery($order); - $deliveryId = $delivery->getId(); - $this->orderDeliveryService->method('getDelivery')->willReturn($delivery); - - // custom fields for shipping are never written - $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); - // api call is never done - $this->mollieApiOrderService->expects($this->never())->method('setShipment'); - // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - public function testMissingLastMollieTransaction(): void - { - $order = $this->createOrder(null); - $customFields[CustomFieldsInterface::MOLLIE_KEY][CustomFieldsInterface::ORDER_KEY] = 'foo'; - $order->setCustomFields($customFields); - $delivery = $this->createDelivery($order); - $deliveryId = $delivery->getId(); - $this->orderDeliveryService->method('getDelivery')->willReturn($delivery); - - // custom fields for shipping are never written - $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); - // api call is never done - $this->mollieApiOrderService->expects($this->never())->method('setShipment'); - // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - public function testThatOrderDeliveryCustomFieldsAreNotWrittenWhenApiCallUnsuccessful(): void - { - $transaction = $this->createTransaction('Kiener\MolliePayments\Handler\Method\FooMethod'); - $order = $this->createOrder($transaction); - $mollieOrderId = 'foo'; - $customFields[CustomFieldsInterface::MOLLIE_KEY][CustomFieldsInterface::ORDER_KEY] = $mollieOrderId; - $order->setCustomFields($customFields); - $salesChannel = $this->getMockBuilder(SalesChannelEntity::class)->disableOriginalConstructor()->getMock(); - $salesChannelId = 'bar'; - $salesChannel->method('getId')->willReturn($salesChannelId); - $order->setSalesChannel($salesChannel); - $order->setSalesChannelId($salesChannelId); - $delivery = $this->createDelivery($order); - $deliveryId = $delivery->getId(); - $this->orderDeliveryService->method('getDelivery')->willReturn($delivery); - $this->mollieApiOrderService->method('setShipment') - ->with($mollieOrderId, null, $salesChannelId) - ->willReturn(false); - - // custom fields for shipping are never written - $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); - - // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - public function testThatOrderDeliveryCustomFieldsAreWrittenWhenApiCallSuccessful(): void - { - $transaction = $this->createTransaction('Kiener\MolliePayments\Handler\Method\FooMethod'); - - $order = $this->createOrder($transaction); - $mollieOrderId = 'foo'; - $customFields[CustomFieldsInterface::MOLLIE_KEY][CustomFieldsInterface::ORDER_KEY] = $mollieOrderId; - $order->setCustomFields($customFields); - $salesChannelId = 'bar'; - $salesChannel = $this->getMockBuilder(SalesChannelEntity::class)->disableOriginalConstructor()->getMock(); - $salesChannel->method('getId')->willReturn($salesChannelId); - $order->setSalesChannel($salesChannel); - $order->setSalesChannelId($salesChannelId); - $delivery = $this->createDelivery($order); - - $deliveryId = $delivery->getId(); - $this->orderDeliveryService->method('getDelivery')->willReturn($delivery); - $this->mollieApiOrderService->method('setShipment') - ->with($mollieOrderId, null,$salesChannelId) - ->willReturn(true); - - // custom fields for shipping are written - $this->orderDeliveryService->expects($this->once()) - ->method('updateCustomFields') - ->with($delivery, [CustomFieldsInterface::DELIVERY_SHIPPED => true], $this->context); - - // result value of facade is true - self::assertTrue($this->mollieShipment->setShipment($deliveryId, $this->context)); - } - - /** - * create a delivery entity and set the order in delivery if given - * - * @param null|OrderEntity $order - * @return OrderDeliveryEntity - */ - private function createDelivery(?OrderEntity $order): OrderDeliveryEntity - { - $delivery = new OrderDeliveryEntity(); - $delivery->setId(Uuid::randomHex()); - $delivery->setTrackingCodes([]); - if ($order instanceof OrderEntity) { - $delivery->setOrder($order); - } - - return $delivery; - } - - /** - * create an order entity and set the transaction in order if given - * - * @param null|OrderTransactionEntity $transaction - * @return OrderEntity - */ - private function createOrder(?OrderTransactionEntity $transaction): OrderEntity - { - $order = new OrderEntity(); - $order->setId(Uuid::randomHex()); - $order->setOrderNumber($this->orderNumber); - $transactions = new OrderTransactionCollection([]); - if ($transaction instanceof OrderTransactionEntity) { - $transactions->add($transaction); - } - $order->setTransactions($transactions); - - return $order; - } - - /** - * create a transaction with a payment with given payment handler name - * - * @param string $paymentHandlerName - * @return OrderTransactionEntity - */ - private function createTransaction(string $paymentHandlerName): OrderTransactionEntity - { - $transaction = new OrderTransactionEntity(); - $transaction->setId(Uuid::randomHex()); - $paymentMethod = new PaymentMethodEntity(); - $paymentMethod->setId(Uuid::randomHex()); - $paymentMethod->setHandlerIdentifier($paymentHandlerName); - $transaction->setCreatedAt(new \DateTime()); - $transaction->setPaymentMethod($paymentMethod); - - return $transaction; - } -} diff --git a/tests/PHPUnit/Fakes/FakeMollieShipment.php b/tests/PHPUnit/Fakes/FakeMollieShipment.php deleted file mode 100644 index b789281af..000000000 --- a/tests/PHPUnit/Fakes/FakeMollieShipment.php +++ /dev/null @@ -1,131 +0,0 @@ -isFullyShipped = false; - $this->shippedOrderNumber = ''; - } - - - /** - * @return false - */ - public function isFullyShipped(): bool - { - return $this->isFullyShipped; - } - - /** - * @return string - */ - public function getShippedOrderNumber(): string - { - return $this->shippedOrderNumber; - } - - - /** - * @param string $orderDeliveryId - * @param Context $context - * @return bool - */ - public function setShipment(string $orderDeliveryId, Context $context): bool - { - return false; - } - - /** - * @param string $orderId - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return Shipment - */ - public function shipOrderByOrderId(string $orderId, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - $this->isFullyShipped = true; - return new Shipment(new MollieApiClient()); - } - - /** - * @param string $orderNumber - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return Shipment - */ - public function shipOrderByOrderNumber(string $orderNumber, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - $this->isFullyShipped = true; - $this->shippedOrderNumber = $orderNumber; - return new Shipment(new MollieApiClient()); - } - - /** - * @param OrderEntity $order - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return Shipment - */ - public function shipOrder(OrderEntity $order, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - $this->isFullyShipped = true; - $this->shippedOrderNumber = $order->getOrderNumber(); - - return new Shipment(new MollieApiClient()); - } - - public function shipItemByOrderId(string $orderId, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - return new Shipment(new MollieApiClient()); - } - - public function shipItemByOrderNumber(string $orderNumber, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - return new Shipment(new MollieApiClient()); - } - - - /** - * @param OrderEntity $order - * @param string $itemIdentifier - * @param int $quantity - * @param string $trackingCarrier - * @param string $trackingCode - * @param string $trackingUrl - * @param Context $context - * @return Shipment - */ - public function shipItem(OrderEntity $order, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment - { - return new Shipment(new MollieApiClient()); - } -} diff --git a/tests/PHPUnit/Fakes/FakeShipment.php b/tests/PHPUnit/Fakes/FakeShipment.php new file mode 100644 index 000000000..3d0cad06d --- /dev/null +++ b/tests/PHPUnit/Fakes/FakeShipment.php @@ -0,0 +1,161 @@ + + */ + private $shippedItems; + + /** + * @var ?ShipmentTrackingInfoStruct + */ + private $shippedTracking; + + /** + * @var int + */ + private $shippedItemQty; + + + /** + * @return string + */ + public function getShippedMollieOrderId(): string + { + return $this->shippedMollieOrderId; + } + + /** + * @return mixed[] + */ + public function getShippedItems(): array + { + return $this->shippedItems; + } + + /** + * @return bool + */ + public function isShipItemCalled(): bool + { + return $this->shipItemCalled; + } + + /** + * @return bool + */ + public function isShipOrderCalled(): bool + { + return $this->shipOrderCalled; + } + + /** + * @return ShipmentTrackingInfoStruct|null + */ + public function getShippedTracking(): ?ShipmentTrackingInfoStruct + { + return $this->shippedTracking; + } + + /** + * @return int + */ + public function getShippedItemQty(): int + { + return $this->shippedItemQty; + } + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @return array|mixed[] + */ + public function getTotals(string $mollieOrderId, string $salesChannelId): array + { + // TODO: Implement getTotals() method. + } + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @return array|mixed[] + */ + public function getStatus(string $mollieOrderId, string $salesChannelId): array + { + // TODO: Implement getStatus() method. + } + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @return ShipmentCollection + */ + public function getShipments(string $mollieOrderId, string $salesChannelId): ShipmentCollection + { + // TODO: Implement getShipments() method. + } + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @param array $items + * @param ShipmentTrackingInfoStruct|null $tracking + * @return MollieShipment + */ + public function shipOrder(string $mollieOrderId, string $salesChannelId, array $items, ?ShipmentTrackingInfoStruct $tracking = null): MollieShipment + { + $this->shipOrderCalled = true; + $this->shippedMollieOrderId = $mollieOrderId; + $this->shippedItems = $items; + $this->shippedTracking = $tracking; + + return new Shipment(new MollieApiClient()); + } + + /** + * @param string $mollieOrderId + * @param string $salesChannelId + * @param string $mollieOrderLineId + * @param int $quantity + * @param ShipmentTrackingInfoStruct|null $tracking + * @return MollieShipment + */ + public function shipItem(string $mollieOrderId, string $salesChannelId, string $mollieOrderLineId, int $quantity, ?ShipmentTrackingInfoStruct $tracking = null): MollieShipment + { + $this->shipItemCalled = true; + $this->shippedMollieOrderId = $mollieOrderId; + $this->shippedItems = [$mollieOrderLineId]; + $this->shippedTracking = $tracking; + $this->shippedItemQty = $quantity; + + return new Shipment(new MollieApiClient()); + } + +} diff --git a/tests/PHPUnit/Fakes/FakeShipmentManager.php b/tests/PHPUnit/Fakes/FakeShipmentManager.php new file mode 100644 index 000000000..c76285bda --- /dev/null +++ b/tests/PHPUnit/Fakes/FakeShipmentManager.php @@ -0,0 +1,105 @@ +isFullyShipped = false; + $this->shippedOrderNumber = ''; + } + + + /** + * @return false + */ + public function isFullyShipped(): bool + { + return $this->isFullyShipped; + } + + /** + * @return string + */ + public function getShippedOrderNumber(): string + { + return $this->shippedOrderNumber; + } + + /** + * @param OrderEntity $order + * @param TrackingData|null $tracking + * @param array $shippingItems + * @param Context $context + * @return Shipment + */ + public function shipOrder(OrderEntity $order, ?TrackingData $tracking, array $shippingItems, Context $context): Shipment + { + $this->isFullyShipped = true; + $this->shippedOrderNumber = $order->getOrderNumber(); + + return new Shipment(new MollieApiClient()); + } + + /** + * @param OrderEntity $order + * @param string $itemIdentifier + * @param int $quantity + * @param TrackingData|null $tracking + * @param Context $context + * @return Shipment + */ + public function shipItem(OrderEntity $order, string $itemIdentifier, int $quantity, ?TrackingData $tracking, Context $context): Shipment + { + return new Shipment(new MollieApiClient()); + } + + /** + * @param OrderEntity $order + * @param TrackingData|null $tracking + * @param Context $context + * @return Shipment + */ + public function shipOrderRest(OrderEntity $order, ?TrackingData $tracking, Context $context): Shipment + { + $this->isFullyShipped = true; + $this->shippedOrderNumber = $order->getOrderNumber(); + + return new Shipment(new MollieApiClient()); + } + + public function getStatus(string $orderId, Context $context): array + { + // TODO: Implement getStatus() method. + } + + public function getTotals(string $orderId, Context $context): array + { + // TODO: Implement getTotals() method. + } + + +} diff --git a/tests/PHPUnit/Service/MollieApi/OrderTest.php b/tests/PHPUnit/Service/MollieApi/OrderTest.php index 3ed09b1ee..1d7e4d0f0 100644 --- a/tests/PHPUnit/Service/MollieApi/OrderTest.php +++ b/tests/PHPUnit/Service/MollieApi/OrderTest.php @@ -162,7 +162,7 @@ public function getIsCompletelyShippedData() [OrderLineType::TYPE_STORE_CREDIT, 1, false], // These two types are not (yet) being used by the Mollie plugin, so there should not be any order lines - // with these types in the Mollie order, and we cannot ship them using Facade/MollieShipment::shipItem. + // with these types in the Mollie order, and we cannot ship them using Facade/ShipmentManager::shipItem. // Therefore we mark the (Shopware) order completely shipped. [OrderLineType::TYPE_GIFT_CARD, 0, true], [OrderLineType::TYPE_GIFT_CARD, 1, true], diff --git a/tests/PHPUnit/Service/MollieApi/ShipmentTest.php b/tests/PHPUnit/Service/MollieApi/ShipmentTest.php index e3a8e7b56..1251dec14 100644 --- a/tests/PHPUnit/Service/MollieApi/ShipmentTest.php +++ b/tests/PHPUnit/Service/MollieApi/ShipmentTest.php @@ -106,7 +106,7 @@ public function testShipOrder() ->method('shipAll') ->willReturn($this->createMock(MollieShipment::class)); - $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId'); + $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId', [], null); } /** @@ -121,7 +121,7 @@ public function testShipOrderCannotBeShippedException() $this->expectException(MollieOrderCouldNotBeShippedException::class); - $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId'); + $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId', [], null); } /** @@ -134,7 +134,7 @@ public function testShipItem() ->method('createShipment') ->willReturn($this->createMock(MollieShipment::class)); - $this->shipmentApiService->shipItem('mollieOrderId', 'salesChannelId', 'mollieOrderLineId', 1); + $this->shipmentApiService->shipItem('mollieOrderId', 'salesChannelId', 'mollieOrderLineId', 1, null); } /** @@ -149,7 +149,7 @@ public function testShipItemCannotBeShippedException() $this->expectException(MollieOrderCouldNotBeShippedException::class); - $this->shipmentApiService->shipItem('mollieOrderId', 'salesChannelId', 'mollieOrderLineId', 1); + $this->shipmentApiService->shipItem('mollieOrderId', 'salesChannelId', 'mollieOrderLineId', 1, null); } /** diff --git a/tests/PHPUnit/Service/OrderServiceTest.php b/tests/PHPUnit/Service/OrderServiceTest.php index 8b8121b26..4a0d60519 100644 --- a/tests/PHPUnit/Service/OrderServiceTest.php +++ b/tests/PHPUnit/Service/OrderServiceTest.php @@ -6,6 +6,8 @@ use Kiener\MolliePayments\Exception\CouldNotExtractMollieOrderLineIdException; use Kiener\MolliePayments\Exception\OrderNumberNotFoundException; use Kiener\MolliePayments\Service\CustomFieldsInterface; +use Kiener\MolliePayments\Service\DeliveryService; +use Kiener\MolliePayments\Service\OrderDeliveryService; use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\UpdateOrderCustomFields; use Kiener\MolliePayments\Service\UpdateOrderTransactionCustomFields; @@ -51,6 +53,7 @@ protected function setUp(): void $this->createMock(\Kiener\MolliePayments\Service\MollieApi\Order::class), $this->createMock(UpdateOrderCustomFields::class), $this->createMock(UpdateOrderTransactionCustomFields::class), + $this->createMock(OrderDeliveryService::class), new NullLogger() ); } diff --git a/tests/PHPUnit/Service/TrackingInfoStructFactoryTest.php b/tests/PHPUnit/Service/TrackingInfoStructFactoryTest.php index b747f326a..15598acdc 100644 --- a/tests/PHPUnit/Service/TrackingInfoStructFactoryTest.php +++ b/tests/PHPUnit/Service/TrackingInfoStructFactoryTest.php @@ -3,9 +3,12 @@ namespace MolliePayments\Tests\Service; +use Kiener\MolliePayments\Components\ShipmentManager\Exceptions\NoDeliveriesFoundExceptions; use Kiener\MolliePayments\Service\TrackingInfoStructFactory; use PHPUnit\Framework\TestCase; +use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryCollection; use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; +use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Checkout\Shipping\ShippingMethodEntity; /** @@ -24,24 +27,31 @@ public function setUp(): void } - public function testInfoStructCreatedByDelivery(): void + /** + * @return void + * @throws \Kiener\MolliePayments\Components\ShipmentManager\Exceptions\NoDeliveriesFoundException + */ + public function testTrackingFromOrder(): void { $expectedCode = '1234'; $expectedCarrier = 'Test carrier'; $expectedUrl = 'https://test.foo?code=1234'; - $deliveryEntity = new OrderDeliveryEntity(); - $deliveryEntity->setUniqueIdentifier('testDelivery'); - $deliveryEntity->setTrackingCodes([ - $expectedCode - ]); $shippingMethod = new ShippingMethodEntity(); $shippingMethod->setName($expectedCarrier); $shippingMethod->setUniqueIdentifier('testShippingMethod'); $shippingMethod->setTrackingUrl('https://test.foo?code=%s'); + $deliveryEntity = new OrderDeliveryEntity(); + $deliveryEntity->setUniqueIdentifier('testDelivery'); $deliveryEntity->setShippingMethod($shippingMethod); - $trackingInfoStruct = $this->factory->createFromDelivery($deliveryEntity); + $deliveryEntity->setTrackingCodes([$expectedCode]); + + $order = new OrderEntity(); + $order->setDeliveries(new OrderDeliveryCollection([$deliveryEntity])); + + + $trackingInfoStruct = $this->factory->trackingFromOrder($order); $this->assertNotNull($trackingInfoStruct); $this->assertSame($expectedCode, $trackingInfoStruct->getCode()); @@ -49,39 +59,49 @@ public function testInfoStructCreatedByDelivery(): void $this->assertSame($expectedCarrier, $trackingInfoStruct->getCarrier()); } + /** + * @return void + * @throws NoDeliveriesFoundExceptions + */ public function testOnlyOneCodeAccepted(): void { + $shippingMethod = new ShippingMethodEntity(); + $shippingMethod->setName('Test carrier'); + $shippingMethod->setUniqueIdentifier('testShippingMethod'); + $shippingMethod->setTrackingUrl('https://test.foo?code=%s'); $deliveryEntity = new OrderDeliveryEntity(); $deliveryEntity->setUniqueIdentifier('testDelivery'); + $deliveryEntity->setShippingMethod($shippingMethod); $deliveryEntity->setTrackingCodes([ '1234', 'test' ]); - $shippingMethod = new ShippingMethodEntity(); - $shippingMethod->setName('Test carrier'); - $shippingMethod->setUniqueIdentifier('testShippingMethod'); - $shippingMethod->setTrackingUrl('https://test.foo?code=%s'); + $order = new OrderEntity(); + $order->setDeliveries(new OrderDeliveryCollection([$deliveryEntity])); - $deliveryEntity->setShippingMethod($shippingMethod); - $trackingInfoStruct = $this->factory->createFromDelivery($deliveryEntity); + $trackingInfoStruct = $this->factory->trackingFromOrder($order); $this->assertNull($trackingInfoStruct); } + /** + * @return void + */ public function testInfoStructCreatedByArguments(): void { - $expectedCode = '1234'; - $expectedCarrier = 'Test carrier'; - $trackingInfoStruct = $this->factory->create($expectedCarrier, $expectedCode, 'https://test.foo?code=%s'); - $expectedUrl = 'https://test.foo?code=1234'; + $trackingInfoStruct = $this->factory->create( + 'Test carrier', + '1234', + 'https://test.foo?code=%s' + ); $this->assertNotNull($trackingInfoStruct); - $this->assertSame($expectedCode, $trackingInfoStruct->getCode()); - $this->assertSame($expectedUrl, $trackingInfoStruct->getUrl()); - $this->assertSame($expectedCarrier, $trackingInfoStruct->getCarrier()); + $this->assertSame('1234', $trackingInfoStruct->getCode()); + $this->assertSame('https://test.foo?code=1234', $trackingInfoStruct->getUrl()); + $this->assertSame('Test carrier', $trackingInfoStruct->getCarrier()); } public function testUrlWithCodeIsInvalid(): void @@ -139,22 +159,10 @@ public function testCommaSeparatorHasHigherPriority(): void $this->assertSame($expectedCarrier, $trackingInfoStruct->getCarrier()); } + /** - * @dataProvider invalidCodes - * @param string $url - * @param string $trackingCode - * @return void + * @return array */ - public function testInvalidTrackingCodeCharacter(string $trackingCode): void - { - - $trackingInfoStruct = $this->factory->create('test', $trackingCode, 'https://foo.bar/%s'); - $expected = ''; - - $this->assertSame($expected, $trackingInfoStruct->getUrl()); - - } - public function invalidCodes(): array { return [ @@ -167,4 +175,40 @@ public function invalidCodes(): array [str_repeat('1', 200)], ]; } -} \ No newline at end of file + + /** + * @dataProvider invalidCodes + * @param string $invalidCode + * @return void + */ + public function testUrlEmptyOnInvalidCodes(string $invalidCode): void + { + $trackingInfoStruct = $this->factory->create('test', $invalidCode, 'https://foo.bar/%s'); + + $this->assertSame('', $trackingInfoStruct->getUrl()); + } + + + /** + * @return array + */ + public function invalidShippingUrlPatterns(): array + { + return [ + ['%s%'], + ]; + } + + /** + * @dataProvider invalidShippingUrlPatterns + * @param string $invalidPattern + * @return void + */ + public function testUrlEmptyOnInvalidShippingURLs(string $invalidPattern): void + { + $trackingInfoStruct = $this->factory->create('test', 'valid-code', 'https://foo.bar/' . $invalidPattern); + + $this->assertSame('', $trackingInfoStruct->getUrl()); + } + +} diff --git a/tests/PHPUnit/Traits/OrderTrait.php b/tests/PHPUnit/Traits/OrderTrait.php index bad776246..acbdcd34f 100644 --- a/tests/PHPUnit/Traits/OrderTrait.php +++ b/tests/PHPUnit/Traits/OrderTrait.php @@ -9,8 +9,11 @@ use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection; use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection; use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity; +use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryCollection; use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity; +use Shopware\Core\Checkout\Order\OrderEntity; +use Shopware\Core\Checkout\Shipping\ShippingMethodEntity; use Shopware\Core\Content\Media\MediaEntity; use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaCollection; use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaEntity; @@ -25,6 +28,57 @@ trait OrderTrait { + + /** + * @param string $mollieOrderID + * @return OrderEntity + */ + protected function buildMollieOrder(string $mollieOrderID): OrderEntity + { + $order = new OrderEntity(); + $order->setId(Uuid::randomHex()); + $order->setSalesChannelId(Uuid::randomHex()); + + $order->setCustomFields([ + 'mollie_payments' => [ + 'order_id' => $mollieOrderID, + 'payment_id' => 'tr_unit_test', + ]]); + + + $shippingMethod = new ShippingMethodEntity(); + $shippingMethod->setId(Uuid::randomHex()); + $shippingMethod->setName('Test Shipping Method'); + $shippingMethod->setTrackingUrl('https://www.mollie.com/search?q=%s'); + + $delivery = new OrderDeliveryEntity(); + $delivery->setId(Uuid::randomHex()); + $delivery->setTrackingCodes([]); + $delivery->setShippingMethod($shippingMethod); + + $order->setDeliveries(new OrderDeliveryCollection([$delivery])); + + return $order; + } + + /** + * @param string $productNumber + * @return OrderLineItemEntity + */ + protected function buildLineItemEntity(string $productNumber): OrderLineItemEntity + { + $lineItem = new OrderLineItemEntity(); + + $lineItem->setId(Uuid::randomHex()); + + $lineItem->setPayload([ + 'productNumber' => $productNumber, + ]); + + return $lineItem; + } + + public function getCustomerAddressEntity( string $firstName, string $lastName, @@ -78,7 +132,8 @@ public function getOrderLineItem( string $seoUrl = '', string $imageUrl = '', int $position = 1 - ): OrderLineItemEntity { + ): OrderLineItemEntity + { $productId = Uuid::randomHex(); $totalPrice = $quantity * $unitPrice; $calculatedTax = new CalculatedTax($taxAmount, $taxRate, $totalPrice); diff --git a/tests/Swagger/mollie.yaml b/tests/Swagger/mollie.yaml index 0061d784f..e3c345417 100644 --- a/tests/Swagger/mollie.yaml +++ b/tests/Swagger/mollie.yaml @@ -51,7 +51,7 @@ paths: summary: "Search for an order number" description: "Please insert your order number in the POST body." security: - - AdminAPI: [] + - AdminAPI: [ ] requestBody: content: application/json: @@ -95,19 +95,119 @@ paths: "200": description: "successful operation" + /api/mollie/ship/order: + post: + tags: + - "Shipping (Operational)" + summary: "Full shipment (all or rest of items)" + security: + - AdminAPI: [ ] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderNumber: + type: string + description: The Shopware order number + trackingCode: + type: string + description: The tracking code of the order + trackingCarrier: + type: string + description: The tracking carrier of the order + trackingUrl: + type: string + description: The tracking URL of the order + required: + - orderNumber + responses: + "200": + description: "successful operation" - /api/mollie/ship/order?number={number}: - get: + /api/mollie/ship/order/batch: + post: tags: - "Shipping (Operational)" - summary: "Full shipment" + summary: "Full shipment with selected items" security: - - AdminAPI: [] - parameters: - - name: "number" - in: "path" - description: "Shopware order number" - required: true + - AdminAPI: [ ] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderNumber: + type: string + description: The Shopware order number + items: + type: array + items: + type: object + properties: + productNumber: + type: string + description: The Shopware product number + quantity: + type: integer + description: The quantity of the product + default: 1 + trackingCode: + type: string + description: The tracking code of the order + trackingCarrier: + type: string + description: The tracking carrier of the order + trackingUrl: + type: string + description: The tracking URL of the order + required: + - orderNumber + - items + responses: + "200": + description: "successful operation" + + /api/mollie/ship/item: + post: + tags: + - "Shipping (Operational)" + summary: "Ship a provided line item." + security: + - AdminAPI: [ ] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderNumber: + type: string + description: The Shopware order number + productNumber: + type: string + description: The Shopware product number + quantity: + type: integer + description: The quantity of the product + default: 1 + trackingCode: + type: string + description: The tracking code of the order + trackingCarrier: + type: string + description: The tracking carrier of the order + trackingUrl: + type: string + description: The tracking URL of the order + required: + - orderNumber + - productNumber responses: "200": description: "successful operation" @@ -116,9 +216,9 @@ paths: get: tags: - "Shipping (Operational)" - summary: "Partial shipment" + summary: "Ship a provided line item, deprecated - please use the POST request version!" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "order" in: "path" @@ -127,6 +227,7 @@ paths: - name: "item" in: "path" description: "product number" + required: true - name: "quantity" in: "path" description: "quantity" @@ -134,13 +235,29 @@ paths: "200": description: "successful operation" + /api/mollie/ship/order?number={number}: + get: + tags: + - "Shipping (Operational)" + summary: "Full shipment (all or rest of items), deprecated - please use the POST request version!" + security: + - AdminAPI: [ ] + parameters: + - name: "number" + in: "path" + description: "Shopware order number" + required: true + responses: + "200": + description: "successful operation" + /api/mollie/refund/order?number={number}&description={description}: get: tags: - "Refunds (Operational)" summary: "Full Refund" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "number" in: "path" @@ -159,7 +276,7 @@ paths: - "Refunds (Operational)" summary: "Partial Refund" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "number" in: "path" @@ -181,7 +298,7 @@ paths: - "Refunds (Technical)" summary: "Get data from Refund Manager" security: - - AdminAPI: [] + - AdminAPI: [ ] requestBody: content: application/json: @@ -201,7 +318,7 @@ paths: - "Refunds (Technical)" summary: "Refund with Refund Manager" security: - - AdminAPI: [] + - AdminAPI: [ ] requestBody: content: application/json: @@ -248,27 +365,27 @@ paths: description: "successful operation" /mollie/webhook/subscription/{subscriptionId}: - post: - tags: - - "Webhooks" - summary: "Start a subscription renewal or update an existing subscription order and payment status." - parameters: - - in: "path" - name: "subscriptionId" - description: "ID of the Shopware Subscription" - required: true - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - id: - type: string - description: "The matching transaction ID of Mollie that was captured. tr_xyz, ..." - responses: - "200": - description: "successful operation" + post: + tags: + - "Webhooks" + summary: "Start a subscription renewal or update an existing subscription order and payment status." + parameters: + - in: "path" + name: "subscriptionId" + description: "ID of the Shopware Subscription" + required: true + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + id: + type: string + description: "The matching transaction ID of Mollie that was captured. tr_xyz, ..." + responses: + "200": + description: "successful operation" /mollie/webhook/subscription/{subscriptionId}/renew: post: