src/Controller/API/WhiteMark/SaleOrderController.php line 146

Open in your IDE?
  1. <?php
  2. namespace App\Controller\API\WhiteMark;
  3. use App\Entity\SaleOrder;
  4. use App\Entity\User;
  5. use App\Exception\CartException;
  6. use App\Exception\CatalogueException;
  7. use App\Model\Product;
  8. use App\Services\API\VersioningService;
  9. use App\Services\Common\MailerService;
  10. use App\Services\DTV\YamlConfig\YamlReader;
  11. use App\Services\Front\CartService;
  12. use App\Services\Front\Catalogue\JsonCatalogueService;
  13. use App\Services\ProductService;
  14. use DateTime;
  15. use Doctrine\ORM\EntityManagerInterface;
  16. use Exception;
  17. use JMS\Serializer\DeserializationContext;
  18. use JMS\Serializer\SerializerInterface;
  19. use JsonException;
  20. use Nelmio\ApiDocBundle\Annotation\Model;
  21. use OpenApi\Annotations as OA;
  22. use Psr\Cache\InvalidArgumentException;
  23. use Symfony\Component\HttpFoundation\JsonResponse;
  24. use Symfony\Component\HttpFoundation\Request;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\HttpKernel\KernelInterface;
  27. use Symfony\Component\Routing\Annotation\Route;
  28. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  29. use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
  30. use Symfony\Component\Validator\Validator\ValidatorInterface;
  31. use Symfony\Contracts\Cache\ItemInterface;
  32. use Symfony\Contracts\Cache\TagAwareCacheInterface;
  33. use Twig\Environment;
  34. class SaleOrderController extends ApiController
  35. {
  36. private JsonCatalogueService $catalogueService;
  37. private CartService $cartService;
  38. private ProductService $productService;
  39. private YamlReader $yamlReader;
  40. private Environment $twig;
  41. private MailerService $mailerService;
  42. public function __construct(
  43. EntityManagerInterface $em,
  44. SerializerInterface $serializer,
  45. UrlGeneratorInterface $urlGenerator,
  46. ValidatorInterface $validator,
  47. TagAwareCacheInterface $cache,
  48. KernelInterface $kernel,
  49. VersioningService $versioningService,
  50. JsonCatalogueService $catalogueService,
  51. CartService $cartService,
  52. ProductService $productService,
  53. YamlReader $yamlReader,
  54. Environment $twig,
  55. MailerService $mailerService
  56. ) {
  57. parent::__construct($em, $serializer, $urlGenerator, $validator, $cache, $kernel, $versioningService);
  58. $this->catalogueService = $catalogueService;
  59. $this->cartService = $cartService;
  60. $this->productService = $productService;
  61. $this->yamlReader = $yamlReader;
  62. $this->twig = $twig;
  63. $this->mailerService = $mailerService;
  64. $this->cacheTagSlug = 'saleOrdersCache';
  65. }
  66. /**
  67. * Récupérer l'ensemble des commandes.
  68. *
  69. * @OA\Response(
  70. * response=200,
  71. * description="Retourne la liste des commandes",
  72. * @OA\JsonContent(
  73. * type="array",
  74. * @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:list"}))
  75. * )
  76. * )
  77. * @OA\Parameter(
  78. * name="page",
  79. * in="query",
  80. * description="La page que l'on veut récupérer",
  81. * @OA\Schema(type="int")
  82. * )
  83. *
  84. * @OA\Parameter(
  85. * name="limit",
  86. * in="query",
  87. * description="Le nombre d'éléments que l'on veut récupérer",
  88. * @OA\Schema(type="int")
  89. * )
  90. * @OA\Tag(name="SaleOrders")
  91. *
  92. * @Route("/sale_orders", name="api_list_sale_order", methods={"GET"})
  93. *
  94. * @throws InvalidArgumentException
  95. */
  96. public function listSaleOrder(Request $request): JsonResponse
  97. {
  98. $page = $request->get('page', 1);
  99. $limit = $request->get('limit', 10);
  100. $context = $this->getContext(["sale_order:list"]);
  101. $idCache = "listSaleOrder-" . $page . "-" . $limit;
  102. $jsonOrders = $this->cache->get($idCache, function (ItemInterface $item) use (
  103. $page,
  104. $limit,
  105. $context
  106. ) {
  107. $item->tag($this->cacheTagSlug);
  108. $orders = $this->em->getRepository(SaleOrder::class)->findAllWithPagination($page, $limit);
  109. return $this->serializer->serialize($orders, 'json', $context);
  110. });
  111. return new JsonResponse($jsonOrders, Response::HTTP_OK, [], true);
  112. }
  113. /**
  114. * Récupérer l'ensemble des commandes qui ont été update depuis une date.
  115. *
  116. * @OA\Response(
  117. * response=200,
  118. * description="Retourne la liste des commandes updated depuis une date",
  119. * @OA\JsonContent(
  120. * type="array",
  121. * @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:list"}))
  122. * )
  123. * )
  124. *
  125. * @OA\Parameter(
  126. * name="start",
  127. * in="query",
  128. * description="Date de début (format Y-m-d)",
  129. * @OA\Schema(type="date")
  130. * )
  131. * @OA\Tag(name="SaleOrders")
  132. *
  133. * @Route("/sale_orders_updated", name="api_list_sale_order_updated", methods={"GET"})
  134. *
  135. */
  136. public function listSaleOrderUpdated(Request $request): JsonResponse
  137. {
  138. $date = $request->get('date', (new DateTime())->format('Y-m-d'));
  139. $context = $this->getContext(["sale_order:updated"]);
  140. $orders = $this->em->getRepository(SaleOrder::class)->findUpdatedWithDate($date);
  141. $jsonOrders = $this->serializer->serialize($orders, 'json', $context);
  142. return new JsonResponse($jsonOrders, Response::HTTP_OK, [], true);
  143. }
  144. /**
  145. * Créer une commande.
  146. *
  147. * @OA\Response(
  148. * response=201,
  149. * description="Retourne la commande créée",
  150. * ),
  151. *
  152. * @OA\RequestBody(
  153. * description="Commande à créer",
  154. * required=true,
  155. * @OA\MediaType(
  156. * mediaType="application/json",
  157. * @OA\Schema(ref=@Model(type=SaleOrder::class, groups={"sale_order:item"})),
  158. * ),
  159. * )
  160. *
  161. * @OA\Response(
  162. * response=400,
  163. * description="Données invalides"
  164. * ),
  165. *
  166. * @OA\Tag(name="SaleOrders")
  167. *
  168. * @Route("/sale_orders", name="api_create_sale_order", methods={"POST"})
  169. *
  170. * @throws Exception
  171. * @throws InvalidArgumentException
  172. */
  173. public function createSaleOrder(Request $request): JsonResponse
  174. {
  175. /** @var SaleOrder $order */
  176. $context = DeserializationContext::create();
  177. $context->setAttribute('target', new SaleOrder()); // Force une nouvelle instance
  178. $order = $this->serializer->deserialize($request->getContent(), SaleOrder::class, 'json', $context);
  179. if(!$order->getShippingAddress()->getEmail()){
  180. $order->getShippingAddress()->setEmail($order->getUser()->getEmail());
  181. }
  182. if(!$order->getShippingAddress()->getMobile()){
  183. if(!$order->getUser()->getMobile()){
  184. return new JsonResponse(
  185. $this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'Mobile is required in the shipping address', 'Le champ mobile est obligatoire pour la livraison'),
  186. Response::HTTP_BAD_REQUEST,
  187. );
  188. }
  189. $order->getShippingAddress()->setMobile($order->getUser()->getMobile());
  190. }
  191. if ($order->getId() !== null) {
  192. throw new Exception('Order already exists');
  193. }
  194. $orderRequest = json_decode($request->getContent(), true);
  195. if (!$order->getUser() instanceof User) {
  196. return new JsonResponse(
  197. $this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'User not found', 'L\'utilisateur n\'existe pas'),
  198. Response::HTTP_BAD_REQUEST,
  199. );
  200. }
  201. $total = 0;
  202. $alerts = [];
  203. foreach ($order->getItems() as $item) {
  204. $product = $this->catalogueService->findProductBySku($item->getSku());
  205. if (!$product instanceof Product) {
  206. return new JsonResponse(
  207. $this->buildErrorContent(
  208. Response::HTTP_BAD_REQUEST,
  209. 'Product not found',
  210. 'Le produit ' . $item->getSku() . ' n\'exsite pas'
  211. ), Response::HTTP_BAD_REQUEST,
  212. );
  213. }
  214. // check validité evasion/participants
  215. if ($product->getType() === Product::TYPE_EVASION) {
  216. foreach ($orderRequest['items'] as $orderRequestItem) {
  217. if ($product->getSku() === $orderRequestItem['sku']) {
  218. if (!isset($orderRequestItem['participants']) || !count($orderRequestItem['participants'])) {
  219. return new JsonResponse(
  220. $this->buildErrorContent(
  221. Response::HTTP_BAD_REQUEST,
  222. 'Participants required',
  223. 'Veuillez renseigner des participants pour le produit ' . $item->getSku()
  224. ), Response::HTTP_BAD_REQUEST,
  225. );
  226. }
  227. }
  228. }
  229. $nbAdultes = (int)$product->getCombinationValueByKey('nombreadultes');
  230. $nbEnfants = (int)$product->getCombinationValueByKey('nombreenfants');
  231. $countAdultes = 0;
  232. $countEnfants = 0;
  233. foreach ($item->getParticipants() as $participant) {
  234. if ($participant->getTypeParticipant() === 'A') {
  235. $countAdultes++;
  236. }
  237. if ($participant->getTypeParticipant() === 'E') {
  238. $countEnfants++;
  239. }
  240. }
  241. if ($nbAdultes !== $countAdultes) {
  242. return new JsonResponse(
  243. $this->buildErrorContent(
  244. Response::HTTP_BAD_REQUEST,
  245. 'Participants "A" error',
  246. 'Veuillez renseigner le bon nombre de participant type "A" pour le produit ' . $item->getSku(
  247. ) . ' (' . $nbAdultes . ' attendus)'
  248. ), Response::HTTP_BAD_REQUEST,
  249. );
  250. }
  251. if ($nbEnfants !== $countEnfants) {
  252. return new JsonResponse(
  253. $this->buildErrorContent(
  254. Response::HTTP_BAD_REQUEST,
  255. 'Participants "E" error',
  256. 'Veuillez renseigner le bon nombre de participant type "E" pour le produit ' . $item->getSku(
  257. ) . ' (' . $nbEnfants . ' attendus)'
  258. ), Response::HTTP_BAD_REQUEST,
  259. );
  260. }
  261. }
  262. try {
  263. $sku = $product->getSku();
  264. $alertStock = $product->getStockAlert();
  265. $stock = $product->getStock() - $this->productService->getStockPending($sku);
  266. $item
  267. ->setSaleOrder($order)
  268. ->setName($product->getName())
  269. ->setDescription($product->getDescription())
  270. ->setReference($product->getReference())
  271. ->setGamme($product->getType())
  272. ->setPriceHT($product->getSalePrice())
  273. ->setPriceTTC($product->getPriceTTC())
  274. ->setTaxableAmount($product->getTaxableAmount())
  275. ->setUnitPoint($this->cartService->getPointByCartItem($product->getSku()))
  276. ->setImageUrl($product->getImages()[0]['name'] ?? '');
  277. if ($product->getType() === Product::TYPE_EVASION) {
  278. foreach ($item->getParticipants() as $participant) {
  279. $participant->setSaleOrderItem($item);
  280. }
  281. }
  282. if (($stock + $item->getQuantity()) <= $alertStock) {
  283. $product->setStock($stock - $item->getQuantity());
  284. $alerts[$sku] = $product;
  285. }
  286. } catch (CartException|CatalogueException $e) {
  287. return new JsonResponse(
  288. $this->buildErrorContent(
  289. Response::HTTP_BAD_REQUEST,
  290. 'Error when creating the order',
  291. $e->getMessage()
  292. ), Response::HTTP_BAD_REQUEST,
  293. );
  294. }
  295. $total += $item->getUnitPoint() * $item->getQuantity();
  296. }
  297. // TODO CALCULER LE SHIPPING PRICE
  298. $order
  299. ->setTotal($total)->setShippingPrice(0)->setShippingMethod('NONE');
  300. // On vérifie les erreurs
  301. $errors = $this->validator->validate($order);
  302. if ($errors->count() > 0) {
  303. return new JsonResponse(
  304. $this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'Validation Failed', $errors),
  305. Response::HTTP_BAD_REQUEST,
  306. );
  307. }
  308. $duplicateOrder = $this->findRecentDuplicateOrder($order);
  309. if ($duplicateOrder instanceof SaleOrder) {
  310. $idDuplicate = $duplicateOrder->getId();
  311. $location = $this->urlGenerator->generate(
  312. 'api_show_sale_order',
  313. ['id' => $idDuplicate],
  314. UrlGeneratorInterface::ABSOLUTE_URL
  315. );
  316. return new JsonResponse(
  317. $this->buildErrorContent(
  318. Response::HTTP_BAD_REQUEST,
  319. 'Duplicate order found',
  320. ["order number : $idDuplicate"]
  321. ), Response::HTTP_BAD_REQUEST, [
  322. 'Location' => $location,
  323. 'X-DTV-Duplicate-Order' => '1',
  324. ], true
  325. );
  326. }
  327. $this->em->persist($order);
  328. $this->em->flush();
  329. $jsonOrder = $this->serializer->serialize($order, 'json', $this->getContext(["sale_order:item"]));
  330. $location = $this->urlGenerator->generate(
  331. 'api_show_sale_order',
  332. ['id' => $order->getId()],
  333. UrlGeneratorInterface::ABSOLUTE_URL
  334. );
  335. // On vide le cache
  336. $this->clearCache();
  337. // @TODO : on envoie un email si au moins l'un des articles de la commande a : stock calculé <= stock alerte
  338. // $mailer = $this->yamlReader->getMailer();
  339. // $admins = $mailer[ 'stock_alert' ] ?? [];
  340. //
  341. // if ( !empty( $alerts ) && !empty( $admins ) ) {
  342. // foreach ( $admins as $admin ) {
  343. // $html = $this->twig->render( 'back/product/email/alert_stock_v2.html.twig', [
  344. // 'admin' => $admin,
  345. // 'alerts' => $alerts,
  346. // ] );
  347. // $this->mailerService->sendMailRaw(
  348. // 'from',
  349. // $admin[ 'email' ],
  350. // 'Alerte stock',
  351. // $html,
  352. // );
  353. // }
  354. // }
  355. return new JsonResponse($jsonOrder, Response::HTTP_CREATED, ["Location" => $location], true);
  356. }
  357. /**
  358. * Récupérer une commande par son id.
  359. *
  360. * @OA\Response(
  361. * response=200,
  362. * description="Retourne une commande",
  363. * @OA\JsonContent(
  364. * type="array",
  365. * @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:item"}))
  366. * )
  367. * )
  368. * @OA\Tag(name="SaleOrders")
  369. *
  370. * @Route("/sale_orders/{id}", name="api_show_sale_order", methods={"GET"})
  371. */
  372. public function showSaleOrder(string $id): JsonResponse
  373. {
  374. $order = $this->em->getRepository(SaleOrder::class)->find($id);
  375. if (!$order instanceof SaleOrder) {
  376. return $this->getResponseEntityNotFound('SaleOrder');
  377. }
  378. $jsonOrder = $this->serializer->serialize($order, 'json', $this->getContext(["sale_order:item"]));
  379. return new JsonResponse($jsonOrder, Response::HTTP_OK, [], true);
  380. }
  381. /**
  382. * Check si une commande identique a été passée il y a très peu de temps
  383. *
  384. * @param SaleOrder $order
  385. *
  386. * @return SaleOrder|null
  387. *
  388. * @throws JsonException
  389. */
  390. private function findRecentDuplicateOrder(SaleOrder $order): ?SaleOrder
  391. {
  392. $user = $order->getUser();
  393. if (!$user instanceof User) {
  394. return null;
  395. }
  396. $from = new DateTime('-2 minutes');
  397. $recentOrders = $this->em->getRepository(SaleOrder::class)->findRecentByUser($user, $from);
  398. if (!empty($recentOrders)) {
  399. $orderSignature = $this->buildOrderSignature($order);
  400. foreach ($recentOrders as $recentOrder) {
  401. if ($this->buildOrderSignature($recentOrder) === $orderSignature) {
  402. return $recentOrder;
  403. }
  404. }
  405. }
  406. return null;
  407. }
  408. /**
  409. * @param SaleOrder $order
  410. *
  411. * @return string
  412. *
  413. * @throws JsonException
  414. */
  415. private function buildOrderSignature(SaleOrder $order): string
  416. {
  417. $signature = [];
  418. foreach ($order->getItems() as $item) {
  419. $sku = $item->getSku();
  420. if (!$sku) {
  421. continue;
  422. }
  423. $signature[$sku] = ($signature[$sku] ?? 0) + (int)$item->getQuantity();
  424. }
  425. ksort($signature);
  426. return json_encode($signature, JSON_THROW_ON_ERROR);
  427. }
  428. }