<?php
declare(strict_types=1);
namespace App\Module\Checkout\Provider;
use App\Module\Account\Entity\User;
use App\Module\Cart\Helper\SapRequestHelper;
use App\Module\Checkout\Exception\CartFromOrderException;
use App\Module\Payment\Factory\StripePaymentIntentFactory;
use App\Module\Payment\Handler\StripeHandler;
use App\Module\Shared\Repository\Commercetools\CartRepository as SdkCartRepository;
use App\Module\Shared\Repository\Commercetools\ShippingMethodRepository;
use App\Store\StoreContext;
use BestIt\CommercetoolsODM\Exception\APIException;
use BestIt\CommercetoolsODM\Repository\CartRepository as OdmCartRepository;
use App\Module\ShortSimpleCheckout\Provider\Cart\CartProviderInterface;
use Commercetools\Core\Builder\Request\RequestBuilder;
use Commercetools\Core\Client;
use Commercetools\Core\Error\ApiServiceException;
use Commercetools\Core\Fixtures\FixtureException;
use Commercetools\Core\IntegrationTests\ResourceFixture;
use Commercetools\Core\Model\Cart\Cart;
use Commercetools\Core\Model\Cart\LineItem;
use Commercetools\Core\Model\Cart\LineItemCollection;
use Commercetools\Core\Model\Common\Address;
use Commercetools\Core\Model\Common\JsonObject;
use Commercetools\Core\Model\CustomField\CustomFieldObject;
use Commercetools\Core\Model\Order\Order;
use Commercetools\Core\Model\Store\StoreReference;
use Commercetools\Core\Model\Type\TypeReference;
use Denios\SharedConstant\Types\CustomTypes;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Security;
use Throwable;
/**
* Get cart data
*
* @author Michel Chowanski <michel.chowanski@bestit-online.de>
* @package App\Module\Checkout\Provider
*/
class CartProvider implements CartProviderInterface
{
/**
* Session key for the anonymous id.
*/
private const ANONYMOUS_ID_SESSION_KEY = 'anonymousId';
/**
* @var Cart|null
*/
private ?Cart $cart;
private OdmCartRepository $odmCartRepository;
private StoreContext $storeContext;
private SessionInterface $session;
private Security $security;
private LoggerInterface $logger;
private ShippingMethodRepository $shippingMethodRepository;
private SdkCartRepository $sdkCartRepository;
private Client $client;
/**
* CartProvider constructor.
*
* @param Client $client
* @param Security $security
* @param OdmCartRepository $odmCartRepository
* @param StoreContext $storeContext
* @param SessionInterface $session
* @param LoggerInterface $cartLogger
* @param ShippingMethodRepository $shippingMethodRepository
* @param SdkCartRepository $sdkCartRepository
*/
public function __construct(
Client $client,
Security $security,
OdmCartRepository $odmCartRepository,
StoreContext $storeContext,
SessionInterface $session,
LoggerInterface $cartLogger,
ShippingMethodRepository $shippingMethodRepository,
SdkCartRepository $sdkCartRepository
) {
$this->odmCartRepository = $odmCartRepository;
$this->client = $client;
$this->storeContext = $storeContext;
$this->security = $security;
$this->session = $session;
$this->logger = $cartLogger;
$this->cart = null;
$this->shippingMethodRepository = $shippingMethodRepository;
$this->sdkCartRepository = $sdkCartRepository;
}
/**
* @return bool
* @throws APIException
*/
public function cartExists(): bool
{
if ($this->cart !== null) {
return true;
}
$user = $this->security->getUser();
if ($user instanceof User) {
/** @var string $customerId */
$customerId = $user->getCustomerId();
if (!empty($user->getPunchoutCustomFields()) && ($user->getPunchoutCustomFields()['po_enable_oci'] === true || $user->getPunchoutCustomFields()['po_enable_cxml'] === true)) {
try {
$cart = $this->getPunchoutCart($customerId);
} catch (CartFromOrderException | APIException | \Commercetools\Core\Error\ApiException | FixtureException $e) {
return false;
}
} else {
$cart = $this->odmCartRepository->findByCustomerId($customerId);
}
if (!$cart) {
return false;
}
} else {
$cart = $this->getAnonymousCart();
if (!$cart) {
return false;
}
}
$this->cart = $this->validateLineItems($cart);
return true;
}
/**
* @return Cart
* @throws APIException
* @throws \BestIt\CommercetoolsODM\Exception\ResponseException
*
*/
public function getCart(): Cart
{
$country = $this->getCountry();
if ($this->cart !== null) {
// refresh from identity map
$this->cart = $this->odmCartRepository->getDocumentManager()->getUnitOfWork()->tryGetById($this->cart->getId());
if ($this->cart->getCountry() !== $country) {
$this->logger->warning('cart country and store country are different. cartId:' . $this->cart->getId());
$this->checkAndUpdateCartCountry($this->cart, $country);
}
return $this->cart;
}
$user = $this->security->getUser();
if ($user instanceof User) {
/** @var string $customerId */
$customerId = $user->getCustomerId();
if (!empty($user->getPunchoutCustomFields()) && ($user->getPunchoutCustomFields()['po_enable_oci'] === true || $user->getPunchoutCustomFields()['po_enable_cxml'] === true)) {
try {
$cart = $this->getPunchoutCart($customerId);
} catch (CartFromOrderException | APIException | \Commercetools\Core\Error\ApiException | FixtureException $e) {
$cart = null;
}
} else {
$cart = $this->odmCartRepository->findByCustomerId($customerId);
}
if (!$cart) {
$this->logger->debug('starting new customer cart for customer.id = ' . $customerId);
$cart = $this->startCustomerCart();
} else {
$this->logger->debug('using existing cart for customer.id = ' . $customerId);
}
$this->addSapData($cart, $user);
} else {
$cart = $this->getAnonymousCart();
if (!$cart || $cart->getCountry() !== $country) {
$cart = $this->startAnonymousCart();
}
}
if ($cart->getCountry() !== $country) {
$this->logger->warning('cart country and store country are different. Should never happend in live. cartId:' . $cart->getId());
$cart = $this->startCustomerCart();
$this->logger->info('start new cart' . $cart->getId());
}
$cart = $this->validateLineItems($cart);
$this->cart = $this->setDefaultShippingMethod($cart);
return $this->cart;
}
/**
* @param Cart $cart
*
* @return Cart
*/
private function setDefaultShippingMethod(Cart $cart): Cart
{
if ($cart->getShippingInfo() === null && $cart->getShippingAddress() !== null) {
$this->odmCartRepository->save($cart, true);
if ($cart->getShippingAddress()->getCountry() !== $cart->getCountry()) {
throw new Exception(
'Shipping Address is not in the Country for Chart ID:' . $cart->getId()
);
}
$shippingMethod = $this->shippingMethodRepository
->byKey(ShippingMethodRepository::DEFAULT_SHIPPING_METHOD_KEY);
$response = $this->sdkCartRepository->updateShippingMethod($cart, $shippingMethod);
$this->odmCartRepository->getDocumentManager()->getUnitOfWork()->registerAsManaged(
$response,
$response->getId(),
$response->getVersion()
);
return $response;
}
return $cart;
}
/**
*
* Search for a cart with a session ID
*
* @param String $cartid
*
* @return ?String
*/
private function getCartIdBySessionId(string $customerId): ?string
{
try {
$query = <<<GRAPHQL
query Sphere(\$customerId: String!) {
carts(limit: 50, sort: ["lastModifiedAt desc"] where: \$customerId) {
count
results {
id
custom {
customFieldsRaw {
name
value
}
}
}
}
}
GRAPHQL;
$request = RequestBuilder::of()->graphQL()->query()->query($query);
$request->addVariable('customerId', sprintf('customerId = "%s"', $customerId));
try {
$response = $this->client->execute($request);
} catch (ApiServiceException $e) {
throw ResourceFixture::toFixtureException($e);
}
$data = json_decode((string)$response->getBody(), true);
if ($data['data']['carts']['count'] > 0) {
foreach ($data['data']['carts']['results'] as $cartData) {
if (array_key_exists('custom', $cartData) && $cartData['custom'] !== null && array_key_exists('customFieldsRaw', $cartData['custom']) && $cartData['custom']['customFieldsRaw'] !== null) {
$key = array_search(session_id(), array_column($cartData['custom']['customFieldsRaw'], 'value'));
if ($key !== false) {
return $cartData['id'];
}
}
}
}
// No cart found, return normal one
return null;
} catch (Throwable $previous) {
throw new CartFromOrderException(
'failed load cart',
0,
$previous
);
}
}
/**
* Validate if cart line items still exist in the shop.
*
* If a line item has no product slug, it is not present in the shop anymore
* and the cart needs to be recalculated.
*
* @param Cart $cart
* @return Cart
* @see https://docs.commercetools.com/http-api-projects-carts#lineitem
*
*/
private function validateLineItems(Cart $cart): Cart
{
foreach ($cart->getLineItems() as $lineItem) {
assert($lineItem instanceof LineItem);
/* @phpstan-ignore-next-line */
if ($lineItem->getProductSlug() === null) {
$cart = $this->odmCartRepository->recalculateCart($cart);
break;
}
}
return $cart;
}
/**
* Returns the anonymous id of the cart.
*
* The anonymous id (session id) has to be persisted since symfony migrates the session
* on login and gives it a new id. This way we can merge carts after login.
*
* @return string
*/
private function getAnonymousCartId(): string
{
$anonymousId = $this->session->get(self::ANONYMOUS_ID_SESSION_KEY);
if (!$anonymousId) {
$anonymousId = $this->session->getId();
$this->session->set(self::ANONYMOUS_ID_SESSION_KEY, $anonymousId);
}
return $anonymousId;
}
/**
* @return Cart|null
*/
public function getAnonymousCart(): ?Cart
{
$this->logger->debug('getting cart for anonymous user with id ' . $this->getAnonymousCartId());
return $this->odmCartRepository->findOneBy(
[sprintf('anonymousId = "%s" and cartState="Active"', $this->getAnonymousCartId())]
);
}
/**
* @param Cart $cart
* @param bool $updateProductData
*
* @return Cart
*/
public function recalculateCart(Cart $cart, bool $updateProductData = false): Cart
{
return $this->odmCartRepository->modify(
$cart,
fn($cart) => $this->odmCartRepository->recalculateCart($cart, $updateProductData)
);
}
/**
* Starts the cart.
*
* @return Cart
*/
private function startCustomerCart(): Cart
{
$user = $this->security->getUser();
assert($user instanceof User);
if ($user->hasSapCustomPrice() || in_array($this->storeContext->getAlias(), ['us', 'ca'], true)) {
$cart = $this->startCart(Cart::TAX_MODE_EXTERNAL_AMOUNT);
} else {
$cart = $this->startCart(Cart::TAX_MODE_PLATFORM);
}
$cart->setCustomerId($user->getCustomerId());
if (!empty($user->getEmail())) {
$cart->setCustomerEmail($user->getEmail());
}
$address = $user->getDefaultBillingAddress();
if ($address !== null) {
$address->setState(null);
$cart->setBillingAddress($address);
}
$address = $user->getDefaultShippingAddress();
if ($address !== null) {
$address->setState(null);
$cart->setShippingAddress($address);
}
return $this->odmCartRepository->save($cart, true);
}
/**
* @return Cart
*/
private function startAnonymousCart(): Cart
{
$this->logger->debug('starting cart for anonymous user with id ' . $this->getAnonymousCartId());
if (in_array($this->storeContext->getAlias(), ['us', 'ca'], true)) {
$cart = $this->startCart(Cart::TAX_MODE_EXTERNAL);
} else {
$cart = $this->startCart(Cart::TAX_MODE_PLATFORM);
}
$cart->setAnonymousId($this->getAnonymousCartId());
return $this->odmCartRepository->save($cart, true);
}
/**
* @return Cart
*/
private function startCart(string $taxType): Cart
{
//sometimes symfony do not remove element after exception. So we remove it if we create new cart
$this->session->remove(StripePaymentIntentFactory::SESSION_PAYMENT_INTENT_ID);
$cart = Cart::fromArray([
'currency' => $this->storeContext->getDefaultCurrencyCode(),
'country' => strtoupper($this->getCountry()),
'custom' => ['type' => TypeReference::ofKey(CustomTypes::ORDER_TYPE)],
'taxMode' => $taxType,
'taxRoundingMode' => Cart::TAX_ROUNDING_MODE_HALF_UP,
]);
if ($taxType === cart::TAX_MODE_PLATFORM) {
//necessary to calculate the Tax in CT, because for CT tax calculation ShippingAddress is needed
$dummyShippingAddress = Address::fromArray(['country' => strtoupper($this->getCountry())]);
$cart->setShippingAddress($dummyShippingAddress);
}
$cart
->setStore(new StoreReference(['key' => $this->storeContext->getKey()]))
->setLineItems(new LineItemCollection());
return $cart;
}
/**
* Create a new cart from given order and attach it to the current user session.
*
* A new cart is needed, because an order can not be modified anymore.
*
* Assuming that the replicate function proper replicate all properties, we don't need to set the custom initialization
* values as we do in self::startCart()
*
* @param Order $order
* @return Cart
* @throws CartFromOrderException
*
*/
public function setCartFromOrder(Order $order): Cart
{
try {
$cart = $this->odmCartRepository->createFromOrder($order);
$this->logger->notice(sprintf('creating new cart from order %s (order.cart.id: %s)', $order->getId(), $order->getCart()->getId()));
$cart = $this->odmCartRepository->save($cart, true);
// invalidate the maybe existing previous cart, else the find cart method might find two or more carts
if ($this->cart) {
$this->logger->debug('replacing old cart');
$this->odmCartRepository->getDocumentManager()->remove($this->cart);
$this->odmCartRepository->getDocumentManager()->flush();
$this->cart = null;
}
$this->cart = $cart;
return $cart;
} catch (Throwable $previous) {
throw new CartFromOrderException(
sprintf(
'Failed to create new cart from order %s: %s',
$order->getId(),
$previous->getMessage()
),
0,
$previous
);
}
}
/**
* @throws Exception
*/
protected function getCountry(): string
{
$currentLocale = $this->storeContext->getLocaleInfo()->getCurrentLocale();
$parts = explode('_', $currentLocale);
if (count($parts) !== 2) {
throw new Exception('Current locale is invalid');
}
return $parts[1];
}
/**
* @param Cart $cart
* @param string $country
*/
protected function checkAndUpdateCartCountry(Cart $cart, string $country): void
{
if ($cart->getCountry() !== $country) {
$cart->setCountry($country);
$this->odmCartRepository->save($cart);
}
}
/**
* @param Cart $cart
* @param User $user
*/
protected function addSapData(Cart $cart, User $user): void
{
if ($user->hasSapCustomPrice() && $user->getCompanyNumber() !== null) {
$customs = $cart->getCustom()->getFields()->toArray();
$customs['customPriceCustomerNumber'] = $user->getCompanyNumber();
$cart->setCustom(CustomFieldObject::fromArray([
'type' => TypeReference::ofKey(CustomTypes::ORDER_TYPE),
'fields' => $customs
]));
$this->odmCartRepository->save($cart, true);
}
}
/**
* @throws CartFromOrderException
* @throws FixtureException
* @throws APIException
* @throws \Commercetools\Core\Error\ApiException
*/
private function getPunchoutCart($customerId):?Cart {
//Is Punchout Session, get cart with session ID
$cartId = $this->getCartIdBySessionId($customerId);
if ($cartId !== null) {
$request = RequestBuilder::of()->carts()->getById($cartId);
try {
$response = $this->client->execute($request);
} catch (ApiServiceException $e) {
throw ResourceFixture::toFixtureException($e);
}
$cart = new Cart();
$cart->setRawData(json_decode((string)$response->getBody(), true));
$this->odmCartRepository->getDocumentManager()->getUnitOfWork()->registerAsManaged(
$cart,
$cart->getId(),
$cart->getVersion()
);
} else {
$cart = $this->odmCartRepository->findByCustomerId($customerId);
}
return $cart;
}
}