<?php declare(strict_types=1);
/**
* PremSoft
* Copyright © 2020 Premsoft - Sven Mittreiter
*
* @copyright Copyright (c) 2020, premsoft - Sven Mittreiter (http://www.premsoft.de)
* @author Sven Mittreiter <info@premsoft.de>
*/
namespace Prems\Plugin\PremsIndividualOffer6\Storefront\Controller;
use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\Aggregate\OfferItem\OfferItemEntity;
use Prems\Plugin\PremsIndividualOffer6\Core\Offer\NotificationService;
use Prems\Plugin\PremsIndividualOffer6\Core\Offer\PdfService;
use Prems\Plugin\PremsIndividualOffer6\Core\Offer\Storefront\OfferService;
use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\OfferDefinition;
use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\OfferEntity;
use Prems\Plugin\PremsIndividualOffer6\Exception\Offer\OfferNotFoundException;
use Prems\Plugin\PremsIndividualOffer6\Core\Offer\Storefront\ConfigService;
use Prems\Plugin\PremsIndividualOffer6\Storefront\Page\Listing\ListingPageLoader;
use Prems\Plugin\PremsIndividualOffer6\Traits;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
use Shopware\Core\Checkout\Cart\Exception\LineItemNotFoundException;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Promotion\Cart\Discount\DiscountLineItem;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\CountResult;
use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
use Shopware\Core\System\SalesChannel\SalesChannel\AbstractContextSwitchRoute;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Storefront\Framework\Cache\Annotation\HttpCache;
/**
* @RouteScope(scopes={"storefront"})
*/
class OfferController extends StorefrontController
{
use Traits\DocumentCreateResponseTrait;
/**
* @var ListingPageLoader
*/
private $listingPageLoader;
/**
* @var OfferService $offerService
*/
private $offerService;
/**
* @var NotificationService $notificationService
*/
private $notificationService;
/**
* @var ConfigService
*/
public $configService;
private PdfService $pdfService;
/**
* @var CartService
*/
private $cartService;
/**
* @var AbstractContextSwitchRoute
*/
private $contextSwitchRoute;
public function __construct(
ListingPageLoader $listingPageLoader,
OfferService $offerService,
NotificationService $notificationService,
ConfigService $configService,
PdfService $pdfService,
CartService $cartService,
AbstractContextSwitchRoute $contextSwitchRoute
) {
$this->listingPageLoader = $listingPageLoader;
$this->offerService = $offerService;
$this->notificationService = $notificationService;
$this->configService = $configService;
$this->pdfService = $pdfService;
$this->cartService = $cartService;
$this->contextSwitchRoute = $contextSwitchRoute;
}
/**
* Renders the PDF
*
* @param Request $request
* @param SalesChannelContext $salesChannelContext
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @HttpCache()
* @Route("/offer/renderpdf/{offerId}", name="frontend.PremsIndividualOffer.renderpdf", methods={"POST", "GET"}, defaults={"XmlHttpRequest": false})
*/
public function renderPdf(Request $request, SalesChannelContext $salesChannelContext): Response
{
$this->denyAccessUnlessLoggedIn();
$offerId = $request->attributes->get('offerId');
$pdfFile = $this->pdfService->createPdfForOffer($offerId, $salesChannelContext);
$download = $request->query->getBoolean('download', false);
return $this->createResponse($pdfFile->getFileName(), $pdfFile->getFileBlob(), $download, $pdfFile->getContentType());
}
/**
* Show all offers of a customer. If not logged in then redirected to login page
* @Route(
* "/account/offer",
* name="frontend.PremsIndividualOffer.index",
* options={"seo"="false"},
* methods={"GET"},
* defaults={"XmlHttpRequest"=true}
* )
*
* @param SalesChannelContext $context
* @param Request $request
*
* @return Response
*/
public function offerOverview(SalesChannelContext $context, Request $request): Response
{
$session = $request->getSession();
$this->denyAccessUnlessLoggedIn();
$page = $this->listingPageLoader->load($request, $context);
$offerSettings = $this->configService->getConfig($context);
$offerStatusAggregations = $this->offerService->getOfferStatusAggregations($context->getCustomer(), $context->getContext());
$openRequestCount = $this->getCountForAggregation($offerStatusAggregations, 'offeredCount');
$receivedCount = $this->getCountForAggregation($offerStatusAggregations, 'receivedCount');
$orderedCount = $this->getCountForAggregation($offerStatusAggregations, 'orderedCount');
$declinedCount = $this->getCountForAggregation($offerStatusAggregations, 'declinedCount');
$offerId = null;
if ($session->has('PremsIndividualOfferId')) {
$offerId = $session->get('PremsIndividualOfferId');
$session->remove('PremsIndividualOfferId');
}
return $this->renderStorefront('@PremsIndividualOffer6/storefront/page/individual_offer/index.html.twig', [
'page' => $page,
'openRequestCount' => $openRequestCount,
'receivedCount' => $receivedCount,
'orderedCount' => $orderedCount,
'declinedCount' => $declinedCount,
'offerSettings' => $offerSettings,
'offerId' => $offerId,
'flashBags' => $session->getFlashBag()->peekAll(),
]);
}
/**
* @Route(
* "/widgets/account/offer/detail/{id}",
* name="frontend.PremsIndividualOffer.ajaxOfferDetail",
* options={"seo"="false"},
* methods={"GET"},
* defaults={"XmlHttpRequest"=true})
*
* @param SalesChannelContext $context
* @param Request $request
*/
public function ajaxOfferDetail(SalesChannelContext $context, Request $request): Response
{
$this->denyAccessUnlessLoggedIn();
$offerSettings = $this->configService->getConfig($context);
$offerId = '';
if ($request->attributes->has('id')) {
$offerId = (string) $request->attributes->get('id');
}
if ($offerId === '') {
throw new MissingRequestParameterException('id');
}
$offer = $this->offerService->getOfferById($offerId, $context->getContext(), $context->getCustomer());
if (!$offer) {
throw new OfferNotFoundException("Offer $offerId not found");
}
$customerId = $context->getCustomer()->getId();
$items = $this->offerService->getItemsFromOffer($offerId, $customerId, $context);
return $this->renderStorefront('@PremsIndividualOffer6/storefront/page/individual_offer/offer-detail-list.html.twig',
[
'offerDetails' => $items,
'offerId' => $offerId,
'offerSettings' => $offerSettings,
'offer' => $offer,
]);
}
/**
* Cancel the offer mode
* @Route(
* "/individual_offer/cancel_offer_mode",
* name="frontend.PremsIndividualOffer.cancelOfferMode",
* options={"seo"="false"},
* methods={"POST"}
* )
*
* @param SalesChannelContext $context
* @param Request $request
*
* @return Response
*/
public function cancelOfferMode(SalesChannelContext $context, Request $request): Response
{
$this->offerService->cancelOfferMode($context, $request->getSession());
$this->addFlash('info', $this->trans('prems-individual-offer.status_messages.cancel_offer_mode'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
/**
* Cancel the offer mode
* @Route(
* "/individual_offer/send-message",
* name="frontend.PremsIndividualOffer.sendMessage",
* options={"seo"="false"},
* methods={"POST"}
* )
*
* @param SalesChannelContext $context
* @param Request $request
*
* @return Response
*/
public function sendMessage(SalesChannelContext $context, Request $request): Response
{
$customer = $context->getCustomer();
$message = $request->request->get('message', '');
$offerId = $request->request->get('offerId');
$offer = $this->offerService->getOfferById($offerId, $context->getContext(), $customer);
if (!empty($customer) && $offer && $message) {
$bag = new ParameterBag([
'message' => $message,
'customerSign' => $request->request->get('customerSign'),
]);
$this->notificationService->customerGeneralMessageToAdmin($context, $offer, $bag);
} else {
return new JsonResponse([
'status' => 0,
]);
}
return new JsonResponse([
'status' => 1,
'message' => $this->trans('prems-individual-offer.messages.message_send_successfully')
]);
}
/**
* Request for an offer
* @Route(
* "/individual_offer/request",
* name="frontend.PremsIndividualOffer.request",
* methods={"POST","GET"}
* )
*
* @param SalesChannelContext $context
* @param Request $request
*
* @return Response
*/
public function request(SalesChannelContext $context, Request $request): Response
{
$session = $request->getSession();
$this->denyAccessUnlessLoggedIn();
$customer = $context->getCustomer();
if ($this->offerService->isOfferRequestAllowed($context)) {
$customerGroup = $context->getCurrentCustomerGroup();
if (!$customerGroup) {
$customerGroup = $context->getFallbackCustomerGroup();
}
$offerSettings = $this->configService->getConfig($context);
$event = $this->offerService->createOfferRequest($customer, $customerGroup, $context);
$nested = $event->getEventByEntityName(OfferDefinition::ENTITY_NAME);
if ($nested) {
foreach ($nested->getIds() as $id) {
$offer = $this->offerService->getOfferById($id, $context->getContext());
$items = $this->offerService->getItemsFromOffer($offer->getId(), $customer->getId(), $context);
/** @var OfferItemEntity $item */
foreach($items as $item) {
//dirty hack, since the twig method is not available in the twig renderer here
$item->decodedLineItem = unserialize(base64_decode($item->getLineItem()), [LineItem::class, DiscountLineItem::class]);
}
$this->notificationService->customerCreatedOfferRequest($context, $offer, $items);
$session->set('PremsIndividualOfferId', $id);
}
}
if ($offerSettings->isClearBasketAfterOfferRequest()) {
// Empty cart
$cart = $this->cartService->getCart($context->getToken(), $context);
$items = $cart->getLineItems();
foreach($items as $item) {
$this->cartService->remove($cart, $item->getId(), $context);
}
}
$this->addFlash('success', $this->trans('prems-individual-offer.status_messages.offer_request_created', ['%offerNumber%' => $offer->getOfferNumber()]));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
} else {
$this->addFlash('error', $this->trans('prems-individual-offer.status_messages.not_allowed_to_create_offer_request'));
return $this->redirectToRoute('frontend.checkout.cart.page');
}
}
/**
* @Route("/individual_offer/line-item/add",
* name="frontend.PremsIndividualOffer.line-item.add",
* methods={"POST","GET"},
* defaults={"XmlHttpRequest"=true})
*
* requires the provided items in the following form
* 'lineItems' => [
* 'anyKey' => [
* 'id' => 'someKey'
* 'quantity' => 2,
* 'type' => 'someType'
* ],
* 'randomKey' => [
* 'id' => 'otherKey'
* 'quantity' => 2,
* 'type' => 'otherType'
* ]
* ]
*
* @param Cart $cart
* @param RequestDataBag $requestDataBag
* @param Request $request
* @param SalesChannelContext $context
* @return Response
*/
public function addLineItems(Cart $cart, RequestDataBag $requestDataBag, Request $request, SalesChannelContext $context): Response
{
$session = $request->getSession();
// Not allowed for guests
if (!($context && $context->getCustomer() && !$context->getCustomer()->getGuest())) {
$this->setPriceRequestDataBeforeLogin($requestDataBag, $session);
}
$this->denyAccessUnlessLoggedIn();
$this->removeCartLineItems($cart, $context, $session);
/** @var RequestDataBag|null $lineItems */
$lineItems = $requestDataBag->get('lineItems');
if (!$lineItems && !($lineItems=$this->getPriceRequestDataAfterLogin($session))) {
throw new MissingRequestParameterException('lineItems');
}
$count = 0;
try {
$items = [];
/** @var RequestDataBag $lineItemData */
foreach ($lineItems as $lineItemData) {
$lineItem = new LineItem(
$lineItemData->getAlnum('id'),
$lineItemData->getAlnum('type'),
$lineItemData->get('referencedId'),
$lineItemData->getInt('quantity', 1)
);
$lineItem->setStackable($lineItemData->getBoolean('stackable', true));
$lineItem->setRemovable($lineItemData->getBoolean('removable', true));
$count += $lineItem->getQuantity();
$items[] = $lineItem;
}
$cart = $this->offerService->addCartItems($cart, $items, $context);
if ($cart->getErrors()->count() <= 0) {
// Adding products to the offer
$customer = $context->getCustomer();
if ($this->offerService->isOfferRequestAllowed($context)) {
$customerGroup = $context->getCurrentCustomerGroup();
if (!$customerGroup) {
$customerGroup = $context->getFallbackCustomerGroup();
}
$event = $this->offerService->createOfferRequest($customer, $customerGroup, $context);
$nested = $event->getEventByEntityName(OfferDefinition::ENTITY_NAME);
if ($nested) {
foreach ($nested->getIds() as $id) {
$offer = $this->offerService->getOfferById($id, $context->getContext());
$items = $this->offerService->getItemsFromOffer($offer->getId(), $customer->getId(), $context);
/** @var OfferItemEntity $item */
foreach($items as $item) {
//dirty hack, since the twig method is not available in the twig renderer here
$item->decodedLineItem = unserialize(base64_decode($item->getLineItem()), [LineItem::class, DiscountLineItem::class]);
}
$this->notificationService->customerCreatedOfferRequest($context, $offer, $items);
$session->set('PremsIndividualOfferId', $id);
}
}
/** @var OfferItemEntity $lineItem */
foreach ($items as $lineItem) {
// Deleting products form the cart after making offer
$id = $lineItem->getProductId();
if (!$cart->has($id)) {
throw new LineItemNotFoundException($id);
}
$cart = $this->offerService->removeCartItem($cart, $id, $context);
}
$this->restoreCartLineItems($cart, $context, $session);
$this->addFlash('success', $this->trans('prems-individual-offer.status_messages.offer_request_created', ['%offerNumber%' => $offer->getOfferNumber()]));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
} else {
$this->addFlash('error', $this->trans('prems-individual-offer.status_messages.not_allowed_to_create_offer_request'));
return $this->createActionResponse($request);
}
}
} catch (ProductNotFoundException $exception) {
$this->addFlash('danger', $this->trans('error.addToCartError'));
}
return $this->createActionResponse($request);
}
/**
* Accept an offer and add to basket
* @Route(
* "/individual_offer/order",
* name="frontend.PremsIndividualOffer.order",
* methods={"GET"}
* )
*
* @param SalesChannelContext $context
* @param Request $request
*
* @return Response
*/
public function order(SalesChannelContext $context, Request $request): Response
{
$session = $request->getSession();
$this->denyAccessUnlessLoggedIn();
$offerId = '';
if ($request->query->has('offerId')) {
$offerId = (string) $request->query->get('offerId');
}
$customer = $context->getCustomer();
if (!$offerId) {
throw new \Exception('No offer id specified');
}
/** @var OfferEntity $offer */
$offer = $this->offerService->getOfferById($offerId, $context->getContext(), $customer);
if (!$offer || !$this->offerService->hasAccessToOffer($offer, $customer) || !$this->offerService->isOfferOrderable($offer)) {
$this->addFlash('danger', $this->trans('prems-individual-offer.status_messages.no_access_to_offer'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
// If offer mode is activated no second offer order is allowed
if ($this->offerService->getOfferMode($context, $session)) {
$this->addFlash('danger', $this->trans('prems-individual-offer.status_messages.no_double_order_offer_mode_active'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
if ($offer->getShippingMethodId()) {
$requestDataBag = new RequestDataBag([SalesChannelContextService::SHIPPING_METHOD_ID => $offer->getShippingMethodId()]);
try {
$this->contextSwitchRoute->switchContext($requestDataBag, $context);
} catch (ConstraintViolationException $exception) {
$this->addFlash('error', $this->trans('prems-individual-offer.status_messages.error_creating_offer_cart'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
}
$this->offerService->addOfferToBasket($offer, $context, $session);
return $this->redirectToRoute('frontend.checkout.cart.page');
}
/**
* Decline an offer
* @Route(
* "/individual_offer/decline",
* name="frontend.PremsIndividualOffer.decline",
* methods={"GET"}
* )
*
* @param SalesChannelContext $context
* @param Request $request
* @return Response
*/
public function decline(SalesChannelContext $context, Request $request): Response
{
$session = $request->getSession();
$this->denyAccessUnlessLoggedIn();
$offerId = '';
if ($request->query->has('offerId')) {
$offerId = (string) $request->query->get('offerId');
}
$customer = $context->getCustomer();
$offer = $this->offerService->getOfferById($offerId, $context->getContext(), $customer);
if (!$offerId) {
throw new \Exception('No offer id specified');
}
if (!$offer || !$this->offerService->hasAccessToOffer($offer, $customer) || !$this->offerService->isOfferDeclineable($offer)) {
$this->addFlash('error', $this->trans('prems-individual-offer.status_messages.no_access_to_offer'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
$this->offerService->declineOffer($offer, $context->getContext());
$this->notificationService->customerDeclinedOffer($context, $offer);
// If current offer is in offer mode (basket) then remove it
if ($this->offerService->getOfferMode($context, $session) == $offerId) {
$this->offerService->cancelOfferMode($context, $session);
}
$this->addFlash('success', $this->trans('prems-individual-offer.status_messages.declined_offer_successfully'));
return $this->redirectToRoute('frontend.PremsIndividualOffer.index');
}
/**
* @param Cart $cart
* @param SalesChannelContext $context
* @param SessionInterface $session
* @return void
*/
private function removeCartLineItems(Cart $cart, SalesChannelContext $context, SessionInterface $session): void
{
$session->remove('premsIndividualOffer.LineItems');
$cartLineItems = [];
// Remove Line Items from cart before adding new one
foreach ($cart->getLineItems() as $lItem) {
$lId = $lItem->getId();
$cartLineItems [$lId] = [
'id' => $lItem->getId(),
'type' => $lItem->getType(),
'referencedId' => $lItem->getReferencedId(),
'quantity' => $lItem->getQuantity(),
'stackable' => $lItem->isStackable(),
'removable' => $lItem->isRemovable(),
];
if (!$cart->has($lId)) {
throw new LineItemNotFoundException($lId);
}
if ($lItem->isRemovable()) {
$this->offerService->removeCartItem($cart, $lId, $context);
}
}
// Add Line items to session to restore them after offer request
$session->set('premsIndividualOffer.LineItems', $cartLineItems);
}
/**
* @param Cart $cart
* @param SalesChannelContext $context
* @param SessionInterface $session
* @return void
*/
private function restoreCartLineItems(Cart $cart, SalesChannelContext $context, SessionInterface $session): void
{
$lineItemsList = $session->get('premsIndividualOffer.LineItems');
$items = [];
foreach ($lineItemsList as $item) {
$lineItem = new LineItem($item['id'], $item['type'], $item['referencedId'], $item['quantity']);
$lineItem->setStackable($item['stackable']);
$lineItem->setRemovable($item['removable']);
$items[] = $lineItem;
}
$this->offerService->addCartItems($cart, $items, $context);
$session->remove('premsIndividualOffer.LineItems');
}
/**
* @param RequestDataBag $requestDataBag
* @param SessionInterface $session
* @return void
*/
private function setPriceRequestDataBeforeLogin(RequestDataBag $requestDataBag, SessionInterface $session): void
{
$session->remove('premsIndividualOffer.storedPriceRequestData');
/** @var RequestDataBag|null $lineItems */
$lineItems = $requestDataBag->get('lineItems');
if (!$lineItems) {
throw new MissingRequestParameterException('lineItems');
}
$session->set('premsIndividualOffer.storedPriceRequestData', $lineItems);
}
/**
* @param SessionInterface $session
* @return RequestDataBag
*/
private function getPriceRequestDataAfterLogin(SessionInterface $session): RequestDataBag
{
return $session->get('premsIndividualOffer.storedPriceRequestData');
}
/**
* @throws CustomerNotLoggedInException
*/
protected function denyAccessUnlessLoggedIn(bool $allowGuest = false): void
{
/** @var RequestStack $requestStack */
$requestStack = $this->get('request_stack');
$request = $requestStack->getCurrentRequest();
if (!$request) {
throw new CustomerNotLoggedInException();
}
/** @var SalesChannelContext|null $context */
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if (
$context
&& $context->getCustomer()
&& (
$allowGuest === true
|| $context->getCustomer()->getGuest() === false
)
) {
return;
}
throw new CustomerNotLoggedInException();
}
/**
* @param AggregationResultCollection $aggregations
* @param string $aggregationName
* @return int
*/
protected function getCountForAggregation(AggregationResultCollection $aggregations, string $aggregationName): int
{
/** @var CountResult $aggregation */
$aggregation = $aggregations->get($aggregationName);
return $aggregation->getCount();
}
}