<?php
namespace App\Controller\API\WhiteMark;
use App\Entity\SaleOrder;
use App\Entity\User;
use App\Exception\CartException;
use App\Exception\CatalogueException;
use App\Model\Product;
use App\Services\API\VersioningService;
use App\Services\Common\MailerService;
use App\Services\DTV\YamlConfig\YamlReader;
use App\Services\Front\CartService;
use App\Services\Front\Catalogue\JsonCatalogueService;
use App\Services\ProductService;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\SerializerInterface;
use JsonException;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Twig\Environment;
class SaleOrderController extends ApiController
{
private JsonCatalogueService $catalogueService;
private CartService $cartService;
private ProductService $productService;
private YamlReader $yamlReader;
private Environment $twig;
private MailerService $mailerService;
public function __construct(
EntityManagerInterface $em,
SerializerInterface $serializer,
UrlGeneratorInterface $urlGenerator,
ValidatorInterface $validator,
TagAwareCacheInterface $cache,
KernelInterface $kernel,
VersioningService $versioningService,
JsonCatalogueService $catalogueService,
CartService $cartService,
ProductService $productService,
YamlReader $yamlReader,
Environment $twig,
MailerService $mailerService
) {
parent::__construct($em, $serializer, $urlGenerator, $validator, $cache, $kernel, $versioningService);
$this->catalogueService = $catalogueService;
$this->cartService = $cartService;
$this->productService = $productService;
$this->yamlReader = $yamlReader;
$this->twig = $twig;
$this->mailerService = $mailerService;
$this->cacheTagSlug = 'saleOrdersCache';
}
/**
* Récupérer l'ensemble des commandes.
*
* @OA\Response(
* response=200,
* description="Retourne la liste des commandes",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:list"}))
* )
* )
* @OA\Parameter(
* name="page",
* in="query",
* description="La page que l'on veut récupérer",
* @OA\Schema(type="int")
* )
*
* @OA\Parameter(
* name="limit",
* in="query",
* description="Le nombre d'éléments que l'on veut récupérer",
* @OA\Schema(type="int")
* )
* @OA\Tag(name="SaleOrders")
*
* @Route("/sale_orders", name="api_list_sale_order", methods={"GET"})
*
* @throws InvalidArgumentException
*/
public function listSaleOrder(Request $request): JsonResponse
{
$page = $request->get('page', 1);
$limit = $request->get('limit', 10);
$context = $this->getContext(["sale_order:list"]);
$idCache = "listSaleOrder-" . $page . "-" . $limit;
$jsonOrders = $this->cache->get($idCache, function (ItemInterface $item) use (
$page,
$limit,
$context
) {
$item->tag($this->cacheTagSlug);
$orders = $this->em->getRepository(SaleOrder::class)->findAllWithPagination($page, $limit);
return $this->serializer->serialize($orders, 'json', $context);
});
return new JsonResponse($jsonOrders, Response::HTTP_OK, [], true);
}
/**
* Récupérer l'ensemble des commandes qui ont été update depuis une date.
*
* @OA\Response(
* response=200,
* description="Retourne la liste des commandes updated depuis une date",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:list"}))
* )
* )
*
* @OA\Parameter(
* name="start",
* in="query",
* description="Date de début (format Y-m-d)",
* @OA\Schema(type="date")
* )
* @OA\Tag(name="SaleOrders")
*
* @Route("/sale_orders_updated", name="api_list_sale_order_updated", methods={"GET"})
*
*/
public function listSaleOrderUpdated(Request $request): JsonResponse
{
$date = $request->get('date', (new DateTime())->format('Y-m-d'));
$context = $this->getContext(["sale_order:updated"]);
$orders = $this->em->getRepository(SaleOrder::class)->findUpdatedWithDate($date);
$jsonOrders = $this->serializer->serialize($orders, 'json', $context);
return new JsonResponse($jsonOrders, Response::HTTP_OK, [], true);
}
/**
* Créer une commande.
*
* @OA\Response(
* response=201,
* description="Retourne la commande créée",
* ),
*
* @OA\RequestBody(
* description="Commande à créer",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(ref=@Model(type=SaleOrder::class, groups={"sale_order:item"})),
* ),
* )
*
* @OA\Response(
* response=400,
* description="Données invalides"
* ),
*
* @OA\Tag(name="SaleOrders")
*
* @Route("/sale_orders", name="api_create_sale_order", methods={"POST"})
*
* @throws Exception
* @throws InvalidArgumentException
*/
public function createSaleOrder(Request $request): JsonResponse
{
/** @var SaleOrder $order */
$context = DeserializationContext::create();
$context->setAttribute('target', new SaleOrder()); // Force une nouvelle instance
$order = $this->serializer->deserialize($request->getContent(), SaleOrder::class, 'json', $context);
if(!$order->getShippingAddress()->getEmail()){
$order->getShippingAddress()->setEmail($order->getUser()->getEmail());
}
if(!$order->getShippingAddress()->getMobile()){
if(!$order->getUser()->getMobile()){
return new JsonResponse(
$this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'Mobile is required in the shipping address', 'Le champ mobile est obligatoire pour la livraison'),
Response::HTTP_BAD_REQUEST,
);
}
$order->getShippingAddress()->setMobile($order->getUser()->getMobile());
}
if ($order->getId() !== null) {
throw new Exception('Order already exists');
}
$orderRequest = json_decode($request->getContent(), true);
if (!$order->getUser() instanceof User) {
return new JsonResponse(
$this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'User not found', 'L\'utilisateur n\'existe pas'),
Response::HTTP_BAD_REQUEST,
);
}
$total = 0;
$alerts = [];
foreach ($order->getItems() as $item) {
$product = $this->catalogueService->findProductBySku($item->getSku());
if (!$product instanceof Product) {
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Product not found',
'Le produit ' . $item->getSku() . ' n\'exsite pas'
), Response::HTTP_BAD_REQUEST,
);
}
// check validité evasion/participants
if ($product->getType() === Product::TYPE_EVASION) {
foreach ($orderRequest['items'] as $orderRequestItem) {
if ($product->getSku() === $orderRequestItem['sku']) {
if (!isset($orderRequestItem['participants']) || !count($orderRequestItem['participants'])) {
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Participants required',
'Veuillez renseigner des participants pour le produit ' . $item->getSku()
), Response::HTTP_BAD_REQUEST,
);
}
}
}
$nbAdultes = (int)$product->getCombinationValueByKey('nombreadultes');
$nbEnfants = (int)$product->getCombinationValueByKey('nombreenfants');
$countAdultes = 0;
$countEnfants = 0;
foreach ($item->getParticipants() as $participant) {
if ($participant->getTypeParticipant() === 'A') {
$countAdultes++;
}
if ($participant->getTypeParticipant() === 'E') {
$countEnfants++;
}
}
if ($nbAdultes !== $countAdultes) {
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Participants "A" error',
'Veuillez renseigner le bon nombre de participant type "A" pour le produit ' . $item->getSku(
) . ' (' . $nbAdultes . ' attendus)'
), Response::HTTP_BAD_REQUEST,
);
}
if ($nbEnfants !== $countEnfants) {
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Participants "E" error',
'Veuillez renseigner le bon nombre de participant type "E" pour le produit ' . $item->getSku(
) . ' (' . $nbEnfants . ' attendus)'
), Response::HTTP_BAD_REQUEST,
);
}
}
try {
$sku = $product->getSku();
$alertStock = $product->getStockAlert();
$stock = $product->getStock() - $this->productService->getStockPending($sku);
$item
->setSaleOrder($order)
->setName($product->getName())
->setDescription($product->getDescription())
->setReference($product->getReference())
->setGamme($product->getType())
->setPriceHT($product->getSalePrice())
->setPriceTTC($product->getPriceTTC())
->setTaxableAmount($product->getTaxableAmount())
->setUnitPoint($this->cartService->getPointByCartItem($product->getSku()))
->setImageUrl($product->getImages()[0]['name'] ?? '');
if ($product->getType() === Product::TYPE_EVASION) {
foreach ($item->getParticipants() as $participant) {
$participant->setSaleOrderItem($item);
}
}
if (($stock + $item->getQuantity()) <= $alertStock) {
$product->setStock($stock - $item->getQuantity());
$alerts[$sku] = $product;
}
} catch (CartException|CatalogueException $e) {
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Error when creating the order',
$e->getMessage()
), Response::HTTP_BAD_REQUEST,
);
}
$total += $item->getUnitPoint() * $item->getQuantity();
}
// TODO CALCULER LE SHIPPING PRICE
$order
->setTotal($total)->setShippingPrice(0)->setShippingMethod('NONE');
// On vérifie les erreurs
$errors = $this->validator->validate($order);
if ($errors->count() > 0) {
return new JsonResponse(
$this->buildErrorContent(Response::HTTP_BAD_REQUEST, 'Validation Failed', $errors),
Response::HTTP_BAD_REQUEST,
);
}
$duplicateOrder = $this->findRecentDuplicateOrder($order);
if ($duplicateOrder instanceof SaleOrder) {
$idDuplicate = $duplicateOrder->getId();
$location = $this->urlGenerator->generate(
'api_show_sale_order',
['id' => $idDuplicate],
UrlGeneratorInterface::ABSOLUTE_URL
);
return new JsonResponse(
$this->buildErrorContent(
Response::HTTP_BAD_REQUEST,
'Duplicate order found',
["order number : $idDuplicate"]
), Response::HTTP_BAD_REQUEST, [
'Location' => $location,
'X-DTV-Duplicate-Order' => '1',
], true
);
}
$this->em->persist($order);
$this->em->flush();
$jsonOrder = $this->serializer->serialize($order, 'json', $this->getContext(["sale_order:item"]));
$location = $this->urlGenerator->generate(
'api_show_sale_order',
['id' => $order->getId()],
UrlGeneratorInterface::ABSOLUTE_URL
);
// On vide le cache
$this->clearCache();
// @TODO : on envoie un email si au moins l'un des articles de la commande a : stock calculé <= stock alerte
// $mailer = $this->yamlReader->getMailer();
// $admins = $mailer[ 'stock_alert' ] ?? [];
//
// if ( !empty( $alerts ) && !empty( $admins ) ) {
// foreach ( $admins as $admin ) {
// $html = $this->twig->render( 'back/product/email/alert_stock_v2.html.twig', [
// 'admin' => $admin,
// 'alerts' => $alerts,
// ] );
// $this->mailerService->sendMailRaw(
// 'from',
// $admin[ 'email' ],
// 'Alerte stock',
// $html,
// );
// }
// }
return new JsonResponse($jsonOrder, Response::HTTP_CREATED, ["Location" => $location], true);
}
/**
* Récupérer une commande par son id.
*
* @OA\Response(
* response=200,
* description="Retourne une commande",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=SaleOrder::class, groups={"sale_order:item"}))
* )
* )
* @OA\Tag(name="SaleOrders")
*
* @Route("/sale_orders/{id}", name="api_show_sale_order", methods={"GET"})
*/
public function showSaleOrder(string $id): JsonResponse
{
$order = $this->em->getRepository(SaleOrder::class)->find($id);
if (!$order instanceof SaleOrder) {
return $this->getResponseEntityNotFound('SaleOrder');
}
$jsonOrder = $this->serializer->serialize($order, 'json', $this->getContext(["sale_order:item"]));
return new JsonResponse($jsonOrder, Response::HTTP_OK, [], true);
}
/**
* Check si une commande identique a été passée il y a très peu de temps
*
* @param SaleOrder $order
*
* @return SaleOrder|null
*
* @throws JsonException
*/
private function findRecentDuplicateOrder(SaleOrder $order): ?SaleOrder
{
$user = $order->getUser();
if (!$user instanceof User) {
return null;
}
$from = new DateTime('-2 minutes');
$recentOrders = $this->em->getRepository(SaleOrder::class)->findRecentByUser($user, $from);
if (!empty($recentOrders)) {
$orderSignature = $this->buildOrderSignature($order);
foreach ($recentOrders as $recentOrder) {
if ($this->buildOrderSignature($recentOrder) === $orderSignature) {
return $recentOrder;
}
}
}
return null;
}
/**
* @param SaleOrder $order
*
* @return string
*
* @throws JsonException
*/
private function buildOrderSignature(SaleOrder $order): string
{
$signature = [];
foreach ($order->getItems() as $item) {
$sku = $item->getSku();
if (!$sku) {
continue;
}
$signature[$sku] = ($signature[$sku] ?? 0) + (int)$item->getQuantity();
}
ksort($signature);
return json_encode($signature, JSON_THROW_ON_ERROR);
}
}