From 651462610a5c2c8209a22992fcb2633cdfa9ce3f Mon Sep 17 00:00:00 2001 From: Christian Dangl Date: Mon, 11 Dec 2023 13:08:44 +0100 Subject: [PATCH] MOL-1225: add batch shipments to administration --- makefile | 8 +- .../FlowBuilder/Actions/ShipOrderAction.php | 24 +- .../Models/ShipmentLineItem.php | 43 ++ .../ShipmentManager/Models/TrackingData.php | 58 ++ .../ShipmentManager/ShipmentManager.php | 383 ++++++++++++ .../ShipmentManagerInterface.php | 54 ++ .../Api/Order/ShippingControllerBase.php | 565 ++++++++++++------ .../Api/PluginConfig/ConfigControllerBase.php | 8 - .../Api/PluginConfig/Sw6/ConfigController.php | 16 - .../PluginConfig/Sw65/ConfigController.php | 17 - src/Facade/MollieShipment.php | 477 --------------- src/Facade/MollieShipmentInterface.php | 82 --- src/Helper/DeliveryStateHelper.php | 144 ----- .../api/mollie-payments-shipping.service.js | 82 ++- .../core/service/utils/array-utils.service.js | 39 ++ .../mollie-ship-order/MollieShipping.js | 2 + .../components/mollie-ship-order/index.js | 72 ++- .../mollie-ship-order.html.twig | 43 +- .../mollie-ship-order/mollie-ship-order.scss | 13 + .../sw-order-line-items-grid.html.twig | 2 +- .../sw-order-detail-general.html.twig | 2 +- .../app/administration/src/snippet/de-DE.json | 7 +- .../app/administration/src/snippet/en-GB.json | 7 +- .../app/administration/src/snippet/nl-NL.json | 7 +- .../core/utils/array-utils.service.spec.js | 62 ++ .../config/compatibility/controller.xml | 3 +- .../config/compatibility/controller_6.5.xml | 3 +- .../compatibility/flowbuilder/6.4.6.0.xml | 2 +- src/Resources/config/services.xml | 5 - src/Resources/config/services/facades.xml | 6 +- src/Resources/config/services/services.xml | 1 + src/Resources/config/services/subscriber.xml | 4 +- .../MollieApi/Models/MollieShippingItem.php | 43 ++ src/Service/MollieApi/Shipment.php | 26 +- src/Service/OrderService.php | 44 +- .../OrderLineItemEntityAttributes.php | 20 + src/Subscriber/OrderDeliverySubscriber.php | 64 +- .../e2e/storefront/shipment/shipment.cy.js | 30 +- .../actions/admin/ShipThroughMollieAction.js | 23 +- .../FullShippingRepository.js | 16 + .../Actions/ShipOrderActionTest.php | 4 +- .../CreateTrackingStructTest.php | 18 +- .../Facade/MollieShipment/SetShipmentTest.php | 19 +- ...ieShipment.php => FakeShipmentManager.php} | 50 +- tests/PHPUnit/Service/MollieApi/OrderTest.php | 2 +- .../Service/MollieApi/ShipmentTest.php | 4 +- tests/PHPUnit/Service/OrderServiceTest.php | 3 + tests/Swagger/mollie.yaml | 105 +++- 48 files changed, 1590 insertions(+), 1122 deletions(-) create mode 100644 src/Components/ShipmentManager/Models/ShipmentLineItem.php create mode 100644 src/Components/ShipmentManager/Models/TrackingData.php create mode 100644 src/Components/ShipmentManager/ShipmentManager.php create mode 100644 src/Components/ShipmentManager/ShipmentManagerInterface.php delete mode 100644 src/Facade/MollieShipment.php delete mode 100644 src/Facade/MollieShipmentInterface.php delete mode 100644 src/Helper/DeliveryStateHelper.php create mode 100644 src/Resources/app/administration/src/core/service/utils/array-utils.service.js create mode 100644 src/Resources/app/administration/src/module/mollie-payments/components/mollie-ship-order/mollie-ship-order.scss create mode 100644 src/Resources/app/administration/tests/core/utils/array-utils.service.spec.js create mode 100644 src/Service/MollieApi/Models/MollieShippingItem.php rename tests/PHPUnit/Fakes/{FakeMollieShipment.php => FakeShipmentManager.php} (57%) diff --git a/makefile b/makefile index e5d53fc5e..6bee99266 100644 --- a/makefile +++ b/makefile @@ -52,13 +52,15 @@ clean: ## Cleans all dependencies and files rm -rf ./src/Resources/public/molllie-payments.js build: ## Installs the plugin, and builds the artifacts using the Shopware build commands. - php switch-composer.php prod - cd ../../.. && export NODE_OPTIONS=--openssl-legacy-provider && shopware-cli extension build custom/plugins/MolliePayments - php switch-composer.php dev # ----------------------------------------------------- # CUSTOM WEBPACK + php switch-composer.php dev cd ./src/Resources/app/storefront && make build -B # ----------------------------------------------------- + php switch-composer.php prod + cd ../../.. && export NODE_OPTIONS=--openssl-legacy-provider && shopware-cli extension build custom/plugins/MolliePayments + php switch-composer.php dev + # ----------------------------------------------------- cd ../../.. && php bin/console --no-debug theme:refresh cd ../../.. && php bin/console --no-debug theme:compile cd ../../.. && php bin/console --no-debug theme:refresh 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/ShipmentManager/Models/ShipmentLineItem.php b/src/Components/ShipmentManager/Models/ShipmentLineItem.php new file mode 100644 index 000000000..2da033466 --- /dev/null +++ b/src/Components/ShipmentManager/Models/ShipmentLineItem.php @@ -0,0 +1,43 @@ +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..677247a02 --- /dev/null +++ b/src/Components/ShipmentManager/ShipmentManager.php @@ -0,0 +1,383 @@ +deliveryTransitionService = $deliveryTransitionService; + $this->mollieApiOrderService = $mollieApiOrderService; + $this->shipmentService = $shipmentService; + $this->orderDeliveryService = $orderDeliveryService; + $this->orderService = $orderService; + $this->orderDataExtractor = $orderDataExtractor; + $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 ShipmentLineItem[] $shippingItems + * @param Context $context + * @throws \Exception + * @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 \Exception('Please provide a valid list of line items that should be shipped!'); + } + + $orderAttr = new OrderAttributes($order); + + $mollieOrderId = $orderAttr->getMollieOrderId(); + + if ($tracking instanceof TrackingData) { + $trackingData = $this->trackingFactory->create($tracking->getCarrier(), $tracking->getCode(), $tracking->getTrackingUrl()); + } else { + # automatically extract from order + $deliveries = $order->getDeliveries(); + if (!$deliveries instanceof OrderDeliveryCollection) { + throw new \Exception('No deliveries found for order with ID ' . $order->getId() . '!'); + } + + # TODO, really take first delivery? + $trackingData = $this->trackingFactory->createFromDelivery($deliveries->first()); + } + + $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 + ); + + $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 + { + $orderAttr = new OrderAttributes($order); + $mollieOrderId = $orderAttr->getMollieOrderId(); + + if ($tracking instanceof TrackingData) { + $trackingData = $this->trackingFactory->create($tracking->getCarrier(), $tracking->getCode(), $tracking->getTrackingUrl()); + } else { + # automatically extract from order + $deliveries = $order->getDeliveries(); + if (!$deliveries instanceof OrderDeliveryCollection) { + throw new \Exception('No deliveries found for order with ID ' . $order->getId() . '!'); + } + + # TODO, really take first delivery? + $trackingData = $this->trackingFactory->createFromDelivery($deliveries->first()); + } + + $shipment = $this->shipmentService->shipOrder( + $mollieOrderId, + $order->getSalesChannelId(), + [], + $trackingData + ); + + $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); + } + + $mollieOrderLineId = $this->orderService->getMollieOrderLineId($lineItem); + + if ($quantity === 0) { + $quantity = $this->mollieApiOrderService->getMollieOrderLine( + $mollieOrderId, + $mollieOrderLineId, + $order->getSalesChannelId() + )->shippableQuantity; + } + + $mollieTracking = null; + + if ($tracking instanceof TrackingData) { + $mollieTracking = $this->trackingFactory->create( + $tracking->getCarrier(), + $tracking->getCode(), + $tracking->getTrackingUrl() + ); + } + + $shipment = $this->shipmentService->shipItem( + $mollieOrderId, + $order->getSalesChannelId(), + $mollieOrderLineId, + $quantity, + $mollieTracking + ); + + $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->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/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/Controller/Api/Order/ShippingControllerBase.php b/src/Controller/Api/Order/ShippingControllerBase.php index 2796f58f6..732fdd7c2 100644 --- a/src/Controller/Api/Order/ShippingControllerBase.php +++ b/src/Controller/Api/Order/ShippingControllerBase.php @@ -3,18 +3,25 @@ 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\LineItem\LineItemAttributes; +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,35 +29,103 @@ 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/_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={"GET"}) * * @param QueryDataBag $query * @param Context $context * @return JsonResponse */ - public function shipOrderApi(QueryDataBag $query, Context $context): JsonResponse + public function shipOrderOperational(QueryDataBag $query, Context $context): JsonResponse { + $orderNumber = ''; + $trackingCarrier = ''; + $trackingCode = ''; + $trackingUrl = ''; + try { + $orderNumber = $query->get('number'); $trackingCarrier = $query->get('trackingCarrier', ''); $trackingCode = $query->get('trackingCode', ''); @@ -60,11 +135,115 @@ public function shipOrderApi(QueryDataBag $query, Context $context): JsonRespons throw new \InvalidArgumentException('Missing Argument for Order Number!'); } - $shipment = $this->shipmentFacade->shipOrderByOrderNumber( - $orderNumber, - $trackingCarrier, - $trackingCode, - $trackingUrl, + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + if (!$order instanceof OrderEntity) { + throw new \InvalidArgumentException('Order with Number: ' . $orderNumber . ' not found!'); + } + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipOrderRest( + $order, + $tracking, + $context + ); + + return $this->shipmentToJson($shipment); + } catch (\Exception $e) { + $data = [ + 'orderNumber' => $orderNumber, + 'trackingCarrier' => $trackingCarrier, + 'trackingCode' => $trackingCode, + 'trackingUrl' => $trackingUrl, + ]; + + 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 = ''; + $trackingCarrier = ''; + $trackingCode = ''; + $trackingUrl = ''; + + try { + + $content = $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 === null) { + 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 ); @@ -72,6 +251,7 @@ public function shipOrderApi(QueryDataBag $query, Context $context): JsonRespons } catch (\Exception $e) { $data = [ 'orderNumber' => $orderNumber, + 'items' => $requestItems, 'trackingCarrier' => $trackingCarrier, 'trackingCode' => $trackingCode, 'trackingUrl' => $trackingUrl, @@ -82,14 +262,17 @@ public function shipOrderApi(QueryDataBag $query, Context $context): JsonRespons } /** + * This is the custom operational route for shipping items using the API. + * It can be used by 3rd parties, ERP systems and more. + * * @Route("/api/mollie/ship/item", name="api.mollie.ship.item", methods={"GET"}) * * @param QueryDataBag $query * @param Context $context - * @throws \Exception * @return JsonResponse + * @throws \Exception */ - public function shipItemApi(QueryDataBag $query, Context $context): JsonResponse + public function shipItemOperational(QueryDataBag $query, Context $context): JsonResponse { try { $orderNumber = $query->get('order'); @@ -108,13 +291,19 @@ public function shipItemApi(QueryDataBag $query, Context $context): JsonResponse throw new \InvalidArgumentException('Missing Argument for Item identifier!'); } - $shipment = $this->shipmentFacade->shipItemByOrderNumber( - $orderNumber, + $order = $this->orderService->getOrderByNumber($orderNumber, $context); + + if (!$order instanceof OrderEntity) { + throw new \InvalidArgumentException('Order with Number: ' . $orderNumber . ' not found!'); + } + + $tracking = new TrackingData($trackingCarrier, $trackingCode, $trackingUrl); + + $shipment = $this->shipment->shipItem( + $order, $itemIdentifier, $quantity, - $trackingCarrier, - $trackingCode, - $trackingUrl, + $tracking, $context ); @@ -133,71 +322,30 @@ public function shipItemApi(QueryDataBag $query, Context $context): JsonResponse } } - private function shipmentToJson(Shipment $shipment): 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 - ]; - } - - return $this->json([ - 'id' => $shipment->id, - 'orderId' => $shipment->orderId, - 'createdAt' => $shipment->createdAt, - 'lines' => $lines, - 'tracking' => $shipment->tracking - ]); - } - - /** - * @param Exception $e - * @param array $additionalData - * @return JsonResponse - */ - private function exceptionToJson(Exception $e, array $additionalData = []): JsonResponse - { - $this->logger->error( - $e->getMessage(), - $additionalData - ); - - return $this->json([ - 'error' => get_class($e), - 'message' => $e->getMessage(), - 'data' => $additionalData - ], 400); - } - - // Admin routes - /** + * 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 */ - public function shipOrder(RequestDataBag $data, Context $context): JsonResponse + public function shipOrderAdmin(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipOrderResponse( + $itemsBag = $data->get('items', []); + + $items = []; + if ($itemsBag instanceof RequestDataBag) { + $items = $itemsBag->all(); + } + + return $this->processAdminShipOrder( $data->getAlnum('orderId'), $data->get('trackingCarrier', ''), $data->get('trackingCode', ''), $data->get('trackingUrl', ''), + $items, $context ); } @@ -209,56 +357,37 @@ public function shipOrder(RequestDataBag $data, Context $context): JsonResponse * @param Context $context * @return JsonResponse */ - public function shipOrderLegacy(RequestDataBag $data, Context $context): JsonResponse + public function shipOrderAdminLegacy(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipOrderResponse( + $itemsBag = $data->get('items', []); + + $items = []; + if ($itemsBag instanceof RequestDataBag) { + $items = $itemsBag->all(); + } + + return $this->processAdminShipOrder( $data->getAlnum('orderId'), $data->get('trackingCarrier', ''), $data->get('trackingCode', ''), $data->get('trackingUrl', ''), + $items, $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 - { - 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()); - } - } - - /** + * 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 shipItem(RequestDataBag $data, Context $context): JsonResponse + public function shipItemAdmin(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipItemResponse( + return $this->processShipItem( $data->getAlnum('orderId'), $data->getAlnum('itemId'), $data->getInt('quantity'), @@ -276,9 +405,9 @@ public function shipItem(RequestDataBag $data, Context $context): JsonResponse * @param Context $context * @return JsonResponse */ - public function shipItemLegacy(RequestDataBag $data, Context $context): JsonResponse + public function shipItemAdminLegacy(RequestDataBag $data, Context $context): JsonResponse { - return $this->getShipItemResponse( + return $this->processShipItem( $data->getAlnum('orderId'), $data->getAlnum('itemId'), $data->getInt('quantity'), @@ -289,6 +418,87 @@ public function shipItemLegacy(RequestDataBag $data, Context $context): JsonResp ); } + + /** + * @param string $orderId + * @param Context $context + * @return JsonResponse + */ + private function getTotalResponse(string $orderId, Context $context): JsonResponse + { + try { + $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); + } + + /** + * @param string $orderId + * @param Context $context + * @return JsonResponse + */ + private function getStatusResponse(string $orderId, Context $context): JsonResponse + { + 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); + } + + /** + * @param string $orderId + * @param string $trackingCarrier + * @param string $trackingCode + * @param string $trackingUrl + * @param array $lineItems + * @param Context $context + * @return JsonResponse + */ + private function processAdminShipOrder(string $orderId, string $trackingCarrier, string $trackingCode, string $trackingUrl, array $lineItems, Context $context): JsonResponse + { + 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()); + } + } + /** * @param string $orderId * @param string $itemId @@ -299,15 +509,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 +520,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 +552,70 @@ public function getShipItemResponse( } /** - * @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 + * @param Shipment $shipment * @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/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..f7ddbef7b 100644 --- a/src/Resources/config/services/facades.xml +++ b/src/Resources/config/services/facades.xml @@ -5,8 +5,8 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + @@ -14,10 +14,8 @@ - - diff --git a/src/Resources/config/services/services.xml b/src/Resources/config/services/services.xml index 0a77f60ad..adfe92e94 100644 --- a/src/Resources/config/services/services.xml +++ b/src/Resources/config/services/services.xml @@ -60,6 +60,7 @@ + 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/Shipment.php b/src/Service/MollieApi/Shipment.php index a18cda11c..8ee97fae3 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; @@ -40,22 +41,34 @@ 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 = null): MollieShipment + { try { $options = []; + if ($tracking instanceof ShipmentTrackingInfoStruct) { $options['tracking'] = $tracking->toArray(); } + foreach ($items as $item) { + $options['lines'][] = [ + 'id' => $item->getMollieItemId(), + 'quantity' => $item->getQuantity(), + ]; + } + $mollieOrder = $this->orderApiService->getMollieOrder($mollieOrderId, $salesChannelId); - return $mollieOrder->shipAll($options); + + if (empty($items)) { + return $mollieOrder->shipAll($options); + } else { + return $mollieOrder->createShipment($options); + } } catch (ApiException $e) { throw new MollieOrderCouldNotBeShippedException( $mollieOrderId, @@ -67,6 +80,7 @@ public function shipOrder( } } + /** * @param string $mollieOrderId * @param string $salesChannelId 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/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php b/src/Struct/OrderLineItemEntity/OrderLineItemEntityAttributes.php index 3968d8aae..0678883c8 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 @@ -56,6 +60,12 @@ public function __construct(OrderLineItemEntity $lineItem) { $this->item = $lineItem; + $payload = $lineItem->getPayload(); + + if (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 +77,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 */ @@ -220,4 +239,5 @@ private function getCustomFieldValue(OrderLineItemEntity $lineItem, string $keyN return $foundValue; } + } 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/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/Facade/MollieShipment/CreateTrackingStructTest.php b/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php index 49c03c301..8e778bd03 100644 --- a/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php +++ b/tests/PHPUnit/Facade/MollieShipment/CreateTrackingStructTest.php @@ -3,7 +3,7 @@ namespace MolliePayments\Tests\Facade\MollieShipment; use InvalidArgumentException; -use Kiener\MolliePayments\Facade\MollieShipment; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManager; use Kiener\MolliePayments\Service\MollieApi\Order; use Kiener\MolliePayments\Service\MollieApi\OrderDataExtractor; use Kiener\MolliePayments\Service\MollieApi\Shipment; @@ -73,7 +73,7 @@ public function setUp(): void ]); - $this->shipmentFacade = new MollieShipment( + $this->shipmentFacade = new ShipmentManager( $this->createMock(MolliePaymentExtractor::class), $this->createMock(DeliveryTransitionService::class), $this->createMock(Order::class), @@ -97,21 +97,21 @@ public function testTrackingInfoStructWithEmptyTrackingDataReturnsNull() $this->assertNull($trackingInfoStruct); }); - $this->shipmentFacade->shipOrder($this->order, '', '', '', $this->context); + $this->shipmentFacade->shipOrder($this->order, '', '', '', [], $this->context); } public function testTrackingInfoStructWithMissingTrackingCarrierThrowsException() { $this->expectException(InvalidArgumentException::class); - $this->shipmentFacade->shipOrder($this->order, '', '123456789', '', $this->context); + $this->shipmentFacade->shipOrder($this->order, '', '123456789', '', [], $this->context); } public function testTrackingInfoStructWithMissingTrackingCodeThrowsException() { $this->expectException(InvalidArgumentException::class); - $this->shipmentFacade->shipOrder($this->order, 'Mollie', '', '', $this->context); + $this->shipmentFacade->shipOrder($this->order, 'Mollie', '', '', [], $this->context); } public function testTrackingInfoStructWithCorrectData() @@ -123,6 +123,12 @@ public function testTrackingInfoStructWithCorrectData() $this->assertInstanceOf(ShipmentTrackingInfoStruct::class, $trackingInfoStruct); }); - $this->shipmentFacade->shipOrder($this->order, 'Mollie', '123456789', 'https://foo.bar?code=%s', $this->context); + $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 index bef93d763..ddc4fb696 100644 --- a/tests/PHPUnit/Facade/MollieShipment/SetShipmentTest.php +++ b/tests/PHPUnit/Facade/MollieShipment/SetShipmentTest.php @@ -2,7 +2,7 @@ namespace MolliePayments\Tests\Facade\MollieShipment; -use Kiener\MolliePayments\Facade\MollieShipment; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManager; use Kiener\MolliePayments\Service\CustomerService; use Kiener\MolliePayments\Service\CustomFieldsInterface; use Kiener\MolliePayments\Service\MollieApi\Order; @@ -13,7 +13,6 @@ use Kiener\MolliePayments\Service\OrderService; use Kiener\MolliePayments\Service\TrackingInfoStructFactory; use Kiener\MolliePayments\Service\Transition\DeliveryTransitionService; -use Monolog\Logger; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -69,7 +68,7 @@ class SetShipmentTest extends TestCase private $orderDataExtractor; /** - * @var MollieShipment + * @var ShipmentManager */ private $mollieShipment; @@ -103,7 +102,7 @@ public function setup(): void $trackingStructFactory = new TrackingInfoStructFactory(); - $this->mollieShipment = new MollieShipment( + $this->mollieShipment = new ShipmentManager( $this->extractor, $this->deliveryTransitionService, $this->mollieApiOrderService, @@ -127,7 +126,7 @@ public function testInvalidDeliveryId(): void // 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)); + self::assertFalse($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } public function testMissingOrder(): void @@ -141,7 +140,7 @@ public function testMissingOrder(): void // 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)); + self::assertFalse($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } public function testMissingCustomFieldsInOrder(): void @@ -156,7 +155,7 @@ public function testMissingCustomFieldsInOrder(): void // 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)); + self::assertFalse($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } public function testMissingLastMollieTransaction(): void @@ -173,7 +172,7 @@ public function testMissingLastMollieTransaction(): void // 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)); + self::assertFalse($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } public function testThatOrderDeliveryCustomFieldsAreNotWrittenWhenApiCallUnsuccessful(): void @@ -199,7 +198,7 @@ public function testThatOrderDeliveryCustomFieldsAreNotWrittenWhenApiCallUnsucce $this->orderDeliveryService->expects($this->never())->method('updateCustomFields'); // result value of facade is false - self::assertFalse($this->mollieShipment->setShipment($deliveryId, $this->context)); + self::assertFalse($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } public function testThatOrderDeliveryCustomFieldsAreWrittenWhenApiCallSuccessful(): void @@ -229,7 +228,7 @@ public function testThatOrderDeliveryCustomFieldsAreWrittenWhenApiCallSuccessful ->with($delivery, [CustomFieldsInterface::DELIVERY_SHIPPED => true], $this->context); // result value of facade is true - self::assertTrue($this->mollieShipment->setShipment($deliveryId, $this->context)); + self::assertTrue($this->mollieShipment->markOrderDeliveryShipped($deliveryId, $this->context)); } /** diff --git a/tests/PHPUnit/Fakes/FakeMollieShipment.php b/tests/PHPUnit/Fakes/FakeShipmentManager.php similarity index 57% rename from tests/PHPUnit/Fakes/FakeMollieShipment.php rename to tests/PHPUnit/Fakes/FakeShipmentManager.php index b789281af..6d340da5d 100644 --- a/tests/PHPUnit/Fakes/FakeMollieShipment.php +++ b/tests/PHPUnit/Fakes/FakeShipmentManager.php @@ -2,13 +2,13 @@ namespace MolliePayments\Tests\Fakes; -use Kiener\MolliePayments\Facade\MollieShipmentInterface; +use Kiener\MolliePayments\Components\ShipmentManager\ShipmentManagerInterface; use Mollie\Api\MollieApiClient; use Mollie\Api\Resources\Shipment; use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Framework\Context; -class FakeMollieShipment implements MollieShipmentInterface +class FakeShipmentManager implements ShipmentManagerInterface { /** * @var false @@ -53,37 +53,40 @@ public function getShippedOrderNumber(): string * @param Context $context * @return bool */ - public function setShipment(string $orderDeliveryId, Context $context): bool + public function markOrderDeliveryShipped(string $orderDeliveryId, Context $context): bool { return false; } /** - * @param string $orderId + * @param OrderEntity $order * @param string $trackingCarrier * @param string $trackingCode * @param string $trackingUrl + * @param array $shippingItems * @param Context $context * @return Shipment */ - public function shipOrderByOrderId(string $orderId, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment + public function shipOrder(OrderEntity $order, string $trackingCarrier, string $trackingCode, string $trackingUrl, array $shippingItems, Context $context): Shipment { $this->isFullyShipped = true; + $this->shippedOrderNumber = $order->getOrderNumber(); + return new Shipment(new MollieApiClient()); } /** - * @param string $orderNumber + * @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 shipOrderByOrderNumber(string $orderNumber, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment + public function shipItem(OrderEntity $order, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment { - $this->isFullyShipped = true; - $this->shippedOrderNumber = $orderNumber; return new Shipment(new MollieApiClient()); } @@ -95,37 +98,20 @@ public function shipOrderByOrderNumber(string $orderNumber, string $trackingCarr * @param Context $context * @return Shipment */ - public function shipOrder(OrderEntity $order, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment + public function shipOrderRest(OrderEntity $order, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment { - $this->isFullyShipped = true; - $this->shippedOrderNumber = $order->getOrderNumber(); - - return new Shipment(new MollieApiClient()); + // TODO: Implement shipOrderRest() method. } - public function shipItemByOrderId(string $orderId, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment + public function getStatus(string $orderId, Context $context): array { - return new Shipment(new MollieApiClient()); + // TODO: Implement getStatus() method. } - public function shipItemByOrderNumber(string $orderNumber, string $itemIdentifier, int $quantity, string $trackingCarrier, string $trackingCode, string $trackingUrl, Context $context): Shipment + public function getTotals(string $orderId, Context $context): array { - return new Shipment(new MollieApiClient()); + // TODO: Implement getTotals() method. } - /** - * @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/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..f37b749b7 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', []); } /** @@ -121,7 +121,7 @@ public function testShipOrderCannotBeShippedException() $this->expectException(MollieOrderCouldNotBeShippedException::class); - $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId'); + $this->shipmentApiService->shipOrder('mollieOrderId', 'salesChannelId', []); } /** 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/Swagger/mollie.yaml b/tests/Swagger/mollie.yaml index 0061d784f..0c81bddfd 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: @@ -100,9 +100,9 @@ paths: get: tags: - "Shipping (Operational)" - summary: "Full shipment" + summary: "Full shipment (all or rest of items)" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "number" in: "path" @@ -112,13 +112,58 @@ paths: "200": description: "successful operation" + /api/mollie/ship/order/batch: + post: + tags: + - "Shipping (Operational)" + summary: "Full shipment with selected items" + security: + - 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?order={order}&item={item}&quantity={quantity}: get: tags: - "Shipping (Operational)" - summary: "Partial shipment" + summary: "Ship a provided line item" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "order" in: "path" @@ -140,7 +185,7 @@ paths: - "Refunds (Operational)" summary: "Full Refund" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "number" in: "path" @@ -159,7 +204,7 @@ paths: - "Refunds (Operational)" summary: "Partial Refund" security: - - AdminAPI: [] + - AdminAPI: [ ] parameters: - name: "number" in: "path" @@ -181,7 +226,7 @@ paths: - "Refunds (Technical)" summary: "Get data from Refund Manager" security: - - AdminAPI: [] + - AdminAPI: [ ] requestBody: content: application/json: @@ -201,7 +246,7 @@ paths: - "Refunds (Technical)" summary: "Refund with Refund Manager" security: - - AdminAPI: [] + - AdminAPI: [ ] requestBody: content: application/json: @@ -248,27 +293,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: