<?php
namespace Chann\Channelpilot\Subscriber;
use Chann\Channelpilot\Service\ReturnTrackingService;
use Monolog\Logger;
use Shopware\Core\Checkout\Cart\Exception\OrderDeliveryNotFoundException;
use Shopware\Core\Checkout\Cart\Exception\OrderNotFoundException;
use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions;
use Shopware\Core\System\StateMachine\Event\StateMachineStateChangeEvent;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class DeliveryStateSubscriber implements EventSubscriberInterface
{
/**
* @var OrderTransactionStateHandler
*/
protected $transactionStateHandler;
/**
* @var EntityRepository
*/
protected $orderTransactionRepository;
/**
* @var EntityRepository
*/
protected $orderRepository;
/**
* @var EntityRepository
*/
protected EntityRepository $deliveryRepository;
/**
* @var Logger
*/
protected Logger $logger;
/**
* @var EntityRepositoryInterface
*/
protected EntityRepositoryInterface $salesChannelRepository;
/**
* @var SystemConfigService
*/
protected SystemConfigService $systemConfigService;
/**
* @var int Avoids multiple attempts to cancel the same order
*/
protected $previousAttempts = 0;
private ReturnTrackingService $returnTrackingService;
/**
* @param Logger $logger
* @param SystemConfigService $systemConfigService
* @param EntityRepository $transactionRepository
* @param EntityRepository $orderRepository
* @param EntityRepository $deliveryRepository
* @param OrderTransactionStateHandler $transactionStateHandler
* @param EntityRepositoryInterface $salesChannelRepository
*/
public function __construct(
Logger $logger,
SystemConfigService $systemConfigService,
EntityRepository $transactionRepository,
EntityRepository $orderRepository,
EntityRepository $deliveryRepository,
OrderTransactionStateHandler $transactionStateHandler,
EntityRepositoryInterface $salesChannelRepository,
ReturnTrackingService $returnTrackingService
) {
$this->logger = $logger;
$this->orderTransactionRepository = $transactionRepository;
$this->orderRepository = $orderRepository;
$this->deliveryRepository = $deliveryRepository;
$this->transactionStateHandler = $transactionStateHandler;
$this->salesChannelRepository = $salesChannelRepository;
$this->systemConfigService = $systemConfigService;
$this->returnTrackingService = $returnTrackingService;
require_once __DIR__ . '/../Service/API/ChannelPilotSellerAPI_v4_0.php';
}
/**
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [
'state_machine.order.state_changed' => 'onOrderStateChange',
'state_machine.order_transaction.state_changed' => 'onOrderTransactionStateChange',
'state_machine.order_delivery.state_changed' => 'onOrderDeliveryStateChange',
];
}
/**
* @param StateMachineStateChangeEvent $event
* @throws \Exception
*/
public function onOrderDeliveryStateChange(StateMachineStateChangeEvent $event): void
{
try {
// Execute only once per Event
if ($event->getTransitionSide() !== StateMachineStateChangeEvent::STATE_MACHINE_TRANSITION_SIDE_LEAVE) {
return;
}
//Don't execute on "fully shipped" event if it comes from a "partially shipped" event.
if ($event->getPreviousState()->getTechnicalName() === 'shipped_partially'
&& $event->getNextState()->getTechnicalName() === 'shipped') {
return;
}
//Don't execute if it's not set to "fully shipped" or "partially shipped"
$transitionName = $event->getTransition()->getTransitionName(); // ship_partially, ship
if (StateMachineTransitionActions::ACTION_SHIP !== $transitionName
&& StateMachineTransitionActions::ACTION_SHIP_PARTIALLY !== $transitionName) {
return;
}
$orderDeliveryId = $event->getTransition()->getEntityId();
$criteria = new Criteria([$orderDeliveryId]);
$criteria->addAssociation('order');
$criteria->addAssociation('order.orderCustomer');
$criteria->addAssociation('order.transactions');
$criteria->addAssociation('order.lineItems');
$criteria->addAssociation('shippingMethod');
/** @var OrderDeliveryEntity|null $orderDelivery */
$orderDelivery = $this->deliveryRepository
->search($criteria, $event->getContext())
->first();
if ($orderDelivery === null) {
throw new OrderDeliveryNotFoundException($orderDeliveryId);
}
if ($orderDelivery->getOrder() === null) {
throw new OrderNotFoundException($orderDeliveryId);
}
$order = $orderDelivery->getOrder();
if (!$order->getCustomFields() || !isset($order->getCustomFields()['custom_channelpilot_token'])) {
// No Channelpilot Order
return;
}
$trackingCodes = $orderDelivery->getTrackingCodes() ?: [];
$latestTrackingCode = \array_pop($trackingCodes);
$carrierName = $orderDelivery->getShippingMethod() && $orderDelivery->getShippingMethod()->getName()
? $orderDelivery->getShippingMethod()->getName()
: '';
$delivery = new \CPDelivery(
$order->getOrderNumber(),
$order->getCustomFields()['custom_channelpilot_source'],
true,
$latestTrackingCode ?: '--------',
(new \DateTime())->format('c'),
$carrierName,
$this->returnTrackingService->getReturnTrackingIds($order->getId(), $event->getContext())
);
$customFields = $order->getCustomFields();
$customFields['custom_channelpilot_delivery2send'] = serialize($delivery);
$this->orderRepository->update(
[
[
'id' => $order->getId(),
'customFields' => $customFields
]
],
Context::createDefaultContext()
);
$this->logger->info('Marked order ID ' . $order->getId() . ' for delivery');
} catch (\Exception $e) {
$this->logger->error($e);
throw $e;
}
}
/**
* New Approach on SW 6.4.11
* refund / Cancel payment
* @param StateMachineStateChangeEvent $args
*/
public function onOrderTransactionStateChange(StateMachineStateChangeEvent $args): void
{
try {
if ($this->previousAttempts) {
return;
}
$this->previousAttempts = 1;
if (strtolower($args->getTransition()->getEntityName()) !== 'order_transaction') {
return;
}
if (strtolower($args->getNextState()->getTechnicalName()) !== 'refunded') {
return;
}
// We have a cancelled order.
// Load order
$orderTransactionId = $args->getTransition()->getEntityId();
$criteria = new Criteria([$orderTransactionId]);
$criteria->addAssociation('order');
/** @var OrderTransactionEntity $orderTransaction */
$orderTransaction = $this->orderTransactionRepository
->search($criteria, $args->getContext())
->first();
$order = $orderTransaction->getOrder();
if (!$order) {
return;
}
$customFields = $order->getCustomFields();
if (!isset($customFields['custom_channelpilot_orderjson'])) {
// Not a CP order
return;
}
// Observed https://paragonie.com/blog/2016/04/securely-implementing-de-serialization-in-php by limiting allowable classes and only parsing code-provided input.
/**
* @var \CPOrder $cpOrder
*/
$cpOrder = unserialize($customFields['custom_channelpilot_orderjson'], ["allowed_classes" => ["CPOrder", "CPOrderHeader", "CPOrderSummary", "CPMoney"]]);
//$sums = $cpOrder->summary->totalSumOrderInclDiscount;
//$oCPRefund = new \CPRefund($orderId, (new \DateTime())->format('c'), 'Cancellation', $sums->net, $sums->gross, $sums->tax, $sums->taxRate);
$oCPCancellation = new \CPCancellation(
$cpOrder->orderHeader->orderIdExternal,
$order->getOrderNumber(),
null,
$cpOrder->orderHeader->source,
(new \DateTime())->format('c'),
true,
'------'
);
//$customFields['custom_channelpilot_refund2send'] = serialize($oCPRefund);
$customFields['custom_channelpilot_cancel2send'] = serialize($oCPCancellation);
$this->orderRepository->update(
[
[
'id' => $order->getId(),
'customFields' => $customFields
]
],
Context::createDefaultContext()
);
$this->logger->info('Marked order ID ' . $order->getId() . ' for cancellation');
} catch (\Exception $e) {
$this->logger->error($e);
if (isset($order)) {
$this->apiClient->markAsFailed(
$order->getId(),
Context::createDefaultContext()
);
}
throw $e;
}
}
/**
* refund / Cancel payment
* @param StateMachineStateChangeEvent $args
*/
public function onOrderStateChange(StateMachineStateChangeEvent $args): void
{
try {
if ($this->previousAttempts) {
return;
}
$this->previousAttempts = 1;
if (strtolower($args->getTransition()->getEntityName()) !== 'order') {
return;
}
if (strtolower($args->getNextState()->getTechnicalName()) !== 'cancelled') {
return;
}
// We have a cancelled order.
// Load order
$orderId = $args->getTransition()->getEntityId();
$criteria = new Criteria([$orderId]);
// Currently not needed, hence skipped for performance:
//$criteria->addAssociation('orderCustomer');
//$criteria->addAssociation('transactions');
//$criteria->addAssociation('lineItems');
/** @var OrderEntity $order */
$order = $this->orderRepository
->search($criteria, $args->getContext())
->first();
$customFields = $order->getCustomFields();
if (!isset($customFields['custom_channelpilot_orderjson'])) {
// Not a CP order
return;
}
// Observed https://paragonie.com/blog/2016/04/securely-implementing-de-serialization-in-php by limiting allowable classes and only parsing code-provided input.
/**
* @var \CPOrder $cpOrder
*/
$cpOrder = unserialize($customFields['custom_channelpilot_orderjson'], ["allowed_classes" => ["CPOrder", "CPOrderHeader", "CPOrderSummary", "CPMoney"]]);
//$sums = $cpOrder->summary->totalSumOrderInclDiscount;
//$oCPRefund = new \CPRefund($orderId, (new \DateTime())->format('c'), 'Cancellation', $sums->net, $sums->gross, $sums->tax, $sums->taxRate);
$oCPCancellation = new \CPCancellation(
$cpOrder->orderHeader->orderIdExternal,
$order->getOrderNumber(),
null,
$cpOrder->orderHeader->source,
(new \DateTime())->format('c'),
true,
'------'
);
//$customFields['custom_channelpilot_refund2send'] = serialize($oCPRefund);
$customFields['custom_channelpilot_cancel2send'] = serialize($oCPCancellation);
$this->orderRepository->update(
[
[
'id' => $orderId,
'customFields' => $customFields
]
],
Context::createDefaultContext()
);
$this->logger->info('Marked order ID ' . $orderId . ' for cancellation');
} catch (\Exception $e) {
$this->logger->error($e);
if (isset($order)) {
$this->apiClient->markAsFailed(
$order->getId(),
Context::createDefaultContext()
);
}
throw $e;
}
}
protected function getApi(string $sProvidedToken): \ChannelPilotSellerAPI_v4_0
{
$salesChannelIDs = $this->salesChannelRepository->search(new Criteria(), Context::createDefaultContext())->getIds();
// Find the requested Sales Channel
foreach ($salesChannelIDs as $salesChannelID) {
$token = $this->systemConfigService->get(
'Channelpilot.config.token',
$salesChannelID
);
if ($token === $sProvidedToken) {
// This is the required Saleschannel
// Save MerchantId
$sMerchantId = $this->systemConfigService->get(
'Channelpilot.config.merchantid',
$salesChannelID
);
$api = new \ChannelPilotSellerAPI_v4_0($sMerchantId, $sProvidedToken, $salesChannelID);
return $api;
}
}
$this->logger->error('Could not create Channel Pilot Pro API for token ' . $sProvidedToken);
throw new \RuntimeException('Could not create Channel Pilot Pro API for token ' . $sProvidedToken);
}
}