src/Module/Checkout/Provider/CartProvider.php line 332

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Module\Checkout\Provider;
  4. use App\Module\Account\Entity\User;
  5. use App\Module\Cart\Helper\SapRequestHelper;
  6. use App\Module\Checkout\Exception\CartFromOrderException;
  7. use App\Module\Payment\Factory\StripePaymentIntentFactory;
  8. use App\Module\Payment\Handler\StripeHandler;
  9. use App\Module\Shared\Repository\Commercetools\CartRepository as SdkCartRepository;
  10. use App\Module\Shared\Repository\Commercetools\ShippingMethodRepository;
  11. use App\Store\StoreContext;
  12. use BestIt\CommercetoolsODM\Exception\APIException;
  13. use BestIt\CommercetoolsODM\Repository\CartRepository as OdmCartRepository;
  14. use App\Module\ShortSimpleCheckout\Provider\Cart\CartProviderInterface;
  15. use Commercetools\Core\Builder\Request\RequestBuilder;
  16. use Commercetools\Core\Client;
  17. use Commercetools\Core\Error\ApiServiceException;
  18. use Commercetools\Core\Fixtures\FixtureException;
  19. use Commercetools\Core\IntegrationTests\ResourceFixture;
  20. use Commercetools\Core\Model\Cart\Cart;
  21. use Commercetools\Core\Model\Cart\LineItem;
  22. use Commercetools\Core\Model\Cart\LineItemCollection;
  23. use Commercetools\Core\Model\Common\Address;
  24. use Commercetools\Core\Model\Common\JsonObject;
  25. use Commercetools\Core\Model\CustomField\CustomFieldObject;
  26. use Commercetools\Core\Model\Order\Order;
  27. use Commercetools\Core\Model\Store\StoreReference;
  28. use Commercetools\Core\Model\Type\TypeReference;
  29. use Denios\SharedConstant\Types\CustomTypes;
  30. use Exception;
  31. use Psr\Log\LoggerInterface;
  32. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  33. use Symfony\Component\Security\Core\Security;
  34. use Throwable;
  35. /**
  36. * Get cart data
  37. *
  38. * @author Michel Chowanski <michel.chowanski@bestit-online.de>
  39. * @package App\Module\Checkout\Provider
  40. */
  41. class CartProvider implements CartProviderInterface
  42. {
  43. /**
  44. * Session key for the anonymous id.
  45. */
  46. private const ANONYMOUS_ID_SESSION_KEY = 'anonymousId';
  47. /**
  48. * @var Cart|null
  49. */
  50. private ?Cart $cart;
  51. private OdmCartRepository $odmCartRepository;
  52. private StoreContext $storeContext;
  53. private SessionInterface $session;
  54. private Security $security;
  55. private LoggerInterface $logger;
  56. private ShippingMethodRepository $shippingMethodRepository;
  57. private SdkCartRepository $sdkCartRepository;
  58. private Client $client;
  59. /**
  60. * CartProvider constructor.
  61. *
  62. * @param Client $client
  63. * @param Security $security
  64. * @param OdmCartRepository $odmCartRepository
  65. * @param StoreContext $storeContext
  66. * @param SessionInterface $session
  67. * @param LoggerInterface $cartLogger
  68. * @param ShippingMethodRepository $shippingMethodRepository
  69. * @param SdkCartRepository $sdkCartRepository
  70. */
  71. public function __construct(
  72. Client $client,
  73. Security $security,
  74. OdmCartRepository $odmCartRepository,
  75. StoreContext $storeContext,
  76. SessionInterface $session,
  77. LoggerInterface $cartLogger,
  78. ShippingMethodRepository $shippingMethodRepository,
  79. SdkCartRepository $sdkCartRepository
  80. ) {
  81. $this->odmCartRepository = $odmCartRepository;
  82. $this->client = $client;
  83. $this->storeContext = $storeContext;
  84. $this->security = $security;
  85. $this->session = $session;
  86. $this->logger = $cartLogger;
  87. $this->cart = null;
  88. $this->shippingMethodRepository = $shippingMethodRepository;
  89. $this->sdkCartRepository = $sdkCartRepository;
  90. }
  91. /**
  92. * @return bool
  93. * @throws APIException
  94. */
  95. public function cartExists(): bool
  96. {
  97. if ($this->cart !== null) {
  98. return true;
  99. }
  100. $user = $this->security->getUser();
  101. if ($user instanceof User) {
  102. /** @var string $customerId */
  103. $customerId = $user->getCustomerId();
  104. if (!empty($user->getPunchoutCustomFields()) && ($user->getPunchoutCustomFields()['po_enable_oci'] === true || $user->getPunchoutCustomFields()['po_enable_cxml'] === true)) {
  105. try {
  106. $cart = $this->getPunchoutCart($customerId);
  107. } catch (CartFromOrderException | APIException | \Commercetools\Core\Error\ApiException | FixtureException $e) {
  108. return false;
  109. }
  110. } else {
  111. $cart = $this->odmCartRepository->findByCustomerId($customerId);
  112. }
  113. if (!$cart) {
  114. return false;
  115. }
  116. } else {
  117. $cart = $this->getAnonymousCart();
  118. if (!$cart) {
  119. return false;
  120. }
  121. }
  122. $this->cart = $this->validateLineItems($cart);
  123. return true;
  124. }
  125. /**
  126. * @return Cart
  127. * @throws APIException
  128. * @throws \BestIt\CommercetoolsODM\Exception\ResponseException
  129. *
  130. */
  131. public function getCart(): Cart
  132. {
  133. $country = $this->getCountry();
  134. if ($this->cart !== null) {
  135. // refresh from identity map
  136. $this->cart = $this->odmCartRepository->getDocumentManager()->getUnitOfWork()->tryGetById($this->cart->getId());
  137. if ($this->cart->getCountry() !== $country) {
  138. $this->logger->warning('cart country and store country are different. cartId:' . $this->cart->getId());
  139. $this->checkAndUpdateCartCountry($this->cart, $country);
  140. }
  141. return $this->cart;
  142. }
  143. $user = $this->security->getUser();
  144. if ($user instanceof User) {
  145. /** @var string $customerId */
  146. $customerId = $user->getCustomerId();
  147. if (!empty($user->getPunchoutCustomFields()) && ($user->getPunchoutCustomFields()['po_enable_oci'] === true || $user->getPunchoutCustomFields()['po_enable_cxml'] === true)) {
  148. try {
  149. $cart = $this->getPunchoutCart($customerId);
  150. } catch (CartFromOrderException | APIException | \Commercetools\Core\Error\ApiException | FixtureException $e) {
  151. $cart = null;
  152. }
  153. } else {
  154. $cart = $this->odmCartRepository->findByCustomerId($customerId);
  155. }
  156. if (!$cart) {
  157. $this->logger->debug('starting new customer cart for customer.id = ' . $customerId);
  158. $cart = $this->startCustomerCart();
  159. } else {
  160. $this->logger->debug('using existing cart for customer.id = ' . $customerId);
  161. }
  162. $this->addSapData($cart, $user);
  163. } else {
  164. $cart = $this->getAnonymousCart();
  165. if (!$cart || $cart->getCountry() !== $country) {
  166. $cart = $this->startAnonymousCart();
  167. }
  168. }
  169. if ($cart->getCountry() !== $country) {
  170. $this->logger->warning('cart country and store country are different. Should never happend in live. cartId:' . $cart->getId());
  171. $cart = $this->startCustomerCart();
  172. $this->logger->info('start new cart' . $cart->getId());
  173. }
  174. $cart = $this->validateLineItems($cart);
  175. $this->cart = $this->setDefaultShippingMethod($cart);
  176. return $this->cart;
  177. }
  178. /**
  179. * @param Cart $cart
  180. *
  181. * @return Cart
  182. */
  183. private function setDefaultShippingMethod(Cart $cart): Cart
  184. {
  185. if ($cart->getShippingInfo() === null && $cart->getShippingAddress() !== null) {
  186. $this->odmCartRepository->save($cart, true);
  187. if ($cart->getShippingAddress()->getCountry() !== $cart->getCountry()) {
  188. throw new Exception(
  189. 'Shipping Address is not in the Country for Chart ID:' . $cart->getId()
  190. );
  191. }
  192. $shippingMethod = $this->shippingMethodRepository
  193. ->byKey(ShippingMethodRepository::DEFAULT_SHIPPING_METHOD_KEY);
  194. $response = $this->sdkCartRepository->updateShippingMethod($cart, $shippingMethod);
  195. $this->odmCartRepository->getDocumentManager()->getUnitOfWork()->registerAsManaged(
  196. $response,
  197. $response->getId(),
  198. $response->getVersion()
  199. );
  200. return $response;
  201. }
  202. return $cart;
  203. }
  204. /**
  205. *
  206. * Search for a cart with a session ID
  207. *
  208. * @param String $cartid
  209. *
  210. * @return ?String
  211. */
  212. private function getCartIdBySessionId(string $customerId): ?string
  213. {
  214. try {
  215. $query = <<<GRAPHQL
  216. query Sphere(\$customerId: String!) {
  217. carts(limit: 50, sort: ["lastModifiedAt desc"] where: \$customerId) {
  218. count
  219. results {
  220. id
  221. custom {
  222. customFieldsRaw {
  223. name
  224. value
  225. }
  226. }
  227. }
  228. }
  229. }
  230. GRAPHQL;
  231. $request = RequestBuilder::of()->graphQL()->query()->query($query);
  232. $request->addVariable('customerId', sprintf('customerId = "%s"', $customerId));
  233. try {
  234. $response = $this->client->execute($request);
  235. } catch (ApiServiceException $e) {
  236. throw ResourceFixture::toFixtureException($e);
  237. }
  238. $data = json_decode((string)$response->getBody(), true);
  239. if ($data['data']['carts']['count'] > 0) {
  240. foreach ($data['data']['carts']['results'] as $cartData) {
  241. if (array_key_exists('custom', $cartData) && $cartData['custom'] !== null && array_key_exists('customFieldsRaw', $cartData['custom']) && $cartData['custom']['customFieldsRaw'] !== null) {
  242. $key = array_search(session_id(), array_column($cartData['custom']['customFieldsRaw'], 'value'));
  243. if ($key !== false) {
  244. return $cartData['id'];
  245. }
  246. }
  247. }
  248. }
  249. // No cart found, return normal one
  250. return null;
  251. } catch (Throwable $previous) {
  252. throw new CartFromOrderException(
  253. 'failed load cart',
  254. 0,
  255. $previous
  256. );
  257. }
  258. }
  259. /**
  260. * Validate if cart line items still exist in the shop.
  261. *
  262. * If a line item has no product slug, it is not present in the shop anymore
  263. * and the cart needs to be recalculated.
  264. *
  265. * @param Cart $cart
  266. * @return Cart
  267. * @see https://docs.commercetools.com/http-api-projects-carts#lineitem
  268. *
  269. */
  270. private function validateLineItems(Cart $cart): Cart
  271. {
  272. foreach ($cart->getLineItems() as $lineItem) {
  273. assert($lineItem instanceof LineItem);
  274. /* @phpstan-ignore-next-line */
  275. if ($lineItem->getProductSlug() === null) {
  276. $cart = $this->odmCartRepository->recalculateCart($cart);
  277. break;
  278. }
  279. }
  280. return $cart;
  281. }
  282. /**
  283. * Returns the anonymous id of the cart.
  284. *
  285. * The anonymous id (session id) has to be persisted since symfony migrates the session
  286. * on login and gives it a new id. This way we can merge carts after login.
  287. *
  288. * @return string
  289. */
  290. private function getAnonymousCartId(): string
  291. {
  292. $anonymousId = $this->session->get(self::ANONYMOUS_ID_SESSION_KEY);
  293. if (!$anonymousId) {
  294. $anonymousId = $this->session->getId();
  295. $this->session->set(self::ANONYMOUS_ID_SESSION_KEY, $anonymousId);
  296. }
  297. return $anonymousId;
  298. }
  299. /**
  300. * @return Cart|null
  301. */
  302. public function getAnonymousCart(): ?Cart
  303. {
  304. $this->logger->debug('getting cart for anonymous user with id ' . $this->getAnonymousCartId());
  305. return $this->odmCartRepository->findOneBy(
  306. [sprintf('anonymousId = "%s" and cartState="Active"', $this->getAnonymousCartId())]
  307. );
  308. }
  309. /**
  310. * @param Cart $cart
  311. * @param bool $updateProductData
  312. *
  313. * @return Cart
  314. */
  315. public function recalculateCart(Cart $cart, bool $updateProductData = false): Cart
  316. {
  317. return $this->odmCartRepository->modify(
  318. $cart,
  319. fn($cart) => $this->odmCartRepository->recalculateCart($cart, $updateProductData)
  320. );
  321. }
  322. /**
  323. * Starts the cart.
  324. *
  325. * @return Cart
  326. */
  327. private function startCustomerCart(): Cart
  328. {
  329. $user = $this->security->getUser();
  330. assert($user instanceof User);
  331. if ($user->hasSapCustomPrice() || in_array($this->storeContext->getAlias(), ['us', 'ca'], true)) {
  332. $cart = $this->startCart(Cart::TAX_MODE_EXTERNAL_AMOUNT);
  333. } else {
  334. $cart = $this->startCart(Cart::TAX_MODE_PLATFORM);
  335. }
  336. $cart->setCustomerId($user->getCustomerId());
  337. if (!empty($user->getEmail())) {
  338. $cart->setCustomerEmail($user->getEmail());
  339. }
  340. $address = $user->getDefaultBillingAddress();
  341. if ($address !== null) {
  342. $address->setState(null);
  343. $cart->setBillingAddress($address);
  344. }
  345. $address = $user->getDefaultShippingAddress();
  346. if ($address !== null) {
  347. $address->setState(null);
  348. $cart->setShippingAddress($address);
  349. }
  350. return $this->odmCartRepository->save($cart, true);
  351. }
  352. /**
  353. * @return Cart
  354. */
  355. private function startAnonymousCart(): Cart
  356. {
  357. $this->logger->debug('starting cart for anonymous user with id ' . $this->getAnonymousCartId());
  358. if (in_array($this->storeContext->getAlias(), ['us', 'ca'], true)) {
  359. $cart = $this->startCart(Cart::TAX_MODE_EXTERNAL);
  360. } else {
  361. $cart = $this->startCart(Cart::TAX_MODE_PLATFORM);
  362. }
  363. $cart->setAnonymousId($this->getAnonymousCartId());
  364. return $this->odmCartRepository->save($cart, true);
  365. }
  366. /**
  367. * @return Cart
  368. */
  369. private function startCart(string $taxType): Cart
  370. {
  371. //sometimes symfony do not remove element after exception. So we remove it if we create new cart
  372. $this->session->remove(StripePaymentIntentFactory::SESSION_PAYMENT_INTENT_ID);
  373. $cart = Cart::fromArray([
  374. 'currency' => $this->storeContext->getDefaultCurrencyCode(),
  375. 'country' => strtoupper($this->getCountry()),
  376. 'custom' => ['type' => TypeReference::ofKey(CustomTypes::ORDER_TYPE)],
  377. 'taxMode' => $taxType,
  378. 'taxRoundingMode' => Cart::TAX_ROUNDING_MODE_HALF_UP,
  379. ]);
  380. if ($taxType === cart::TAX_MODE_PLATFORM) {
  381. //necessary to calculate the Tax in CT, because for CT tax calculation ShippingAddress is needed
  382. $dummyShippingAddress = Address::fromArray(['country' => strtoupper($this->getCountry())]);
  383. $cart->setShippingAddress($dummyShippingAddress);
  384. }
  385. $cart
  386. ->setStore(new StoreReference(['key' => $this->storeContext->getKey()]))
  387. ->setLineItems(new LineItemCollection());
  388. return $cart;
  389. }
  390. /**
  391. * Create a new cart from given order and attach it to the current user session.
  392. *
  393. * A new cart is needed, because an order can not be modified anymore.
  394. *
  395. * Assuming that the replicate function proper replicate all properties, we don't need to set the custom initialization
  396. * values as we do in self::startCart()
  397. *
  398. * @param Order $order
  399. * @return Cart
  400. * @throws CartFromOrderException
  401. *
  402. */
  403. public function setCartFromOrder(Order $order): Cart
  404. {
  405. try {
  406. $cart = $this->odmCartRepository->createFromOrder($order);
  407. $this->logger->notice(sprintf('creating new cart from order %s (order.cart.id: %s)', $order->getId(), $order->getCart()->getId()));
  408. $cart = $this->odmCartRepository->save($cart, true);
  409. // invalidate the maybe existing previous cart, else the find cart method might find two or more carts
  410. if ($this->cart) {
  411. $this->logger->debug('replacing old cart');
  412. $this->odmCartRepository->getDocumentManager()->remove($this->cart);
  413. $this->odmCartRepository->getDocumentManager()->flush();
  414. $this->cart = null;
  415. }
  416. $this->cart = $cart;
  417. return $cart;
  418. } catch (Throwable $previous) {
  419. throw new CartFromOrderException(
  420. sprintf(
  421. 'Failed to create new cart from order %s: %s',
  422. $order->getId(),
  423. $previous->getMessage()
  424. ),
  425. 0,
  426. $previous
  427. );
  428. }
  429. }
  430. /**
  431. * @throws Exception
  432. */
  433. protected function getCountry(): string
  434. {
  435. $currentLocale = $this->storeContext->getLocaleInfo()->getCurrentLocale();
  436. $parts = explode('_', $currentLocale);
  437. if (count($parts) !== 2) {
  438. throw new Exception('Current locale is invalid');
  439. }
  440. return $parts[1];
  441. }
  442. /**
  443. * @param Cart $cart
  444. * @param string $country
  445. */
  446. protected function checkAndUpdateCartCountry(Cart $cart, string $country): void
  447. {
  448. if ($cart->getCountry() !== $country) {
  449. $cart->setCountry($country);
  450. $this->odmCartRepository->save($cart);
  451. }
  452. }
  453. /**
  454. * @param Cart $cart
  455. * @param User $user
  456. */
  457. protected function addSapData(Cart $cart, User $user): void
  458. {
  459. if ($user->hasSapCustomPrice() && $user->getCompanyNumber() !== null) {
  460. $customs = $cart->getCustom()->getFields()->toArray();
  461. $customs['customPriceCustomerNumber'] = $user->getCompanyNumber();
  462. $cart->setCustom(CustomFieldObject::fromArray([
  463. 'type' => TypeReference::ofKey(CustomTypes::ORDER_TYPE),
  464. 'fields' => $customs
  465. ]));
  466. $this->odmCartRepository->save($cart, true);
  467. }
  468. }
  469. /**
  470. * @throws CartFromOrderException
  471. * @throws FixtureException
  472. * @throws APIException
  473. * @throws \Commercetools\Core\Error\ApiException
  474. */
  475. private function getPunchoutCart($customerId):?Cart {
  476. //Is Punchout Session, get cart with session ID
  477. $cartId = $this->getCartIdBySessionId($customerId);
  478. if ($cartId !== null) {
  479. $request = RequestBuilder::of()->carts()->getById($cartId);
  480. try {
  481. $response = $this->client->execute($request);
  482. } catch (ApiServiceException $e) {
  483. throw ResourceFixture::toFixtureException($e);
  484. }
  485. $cart = new Cart();
  486. $cart->setRawData(json_decode((string)$response->getBody(), true));
  487. $this->odmCartRepository->getDocumentManager()->getUnitOfWork()->registerAsManaged(
  488. $cart,
  489. $cart->getId(),
  490. $cart->getVersion()
  491. );
  492. } else {
  493. $cart = $this->odmCartRepository->findByCustomerId($customerId);
  494. }
  495. return $cart;
  496. }
  497. }