<?php
namespace App\Controller;
use App\Repository\CategoryRepository;
use App\Repository\ProductRepository;
use App\Services\Cart;
use App\Services\Paypal;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Routing\Annotation\Route;
class CheckoutController extends AbstractController
{
private $paypalService;
public function __construct()
{
$this->paypalService = new Paypal();
}
#[Route('/checkout', name: 'checkout')]
public function index(Request $request, CategoryRepository $catRepo, ProductRepository $productRepo): Response
{
$products = $productRepo->findAll();
$categories = $catRepo->findAll();
//TO COPY TO EVERY ROUTE THAT DISPLAY CART
$session = $request->getSession();
$cart = new Cart($session);
$totalQuantity = $cart->getCartTotalQuantity();
$maxQuantity = $cart->getMaxQuantityAllowed();
$cartProducts = $cart->getCartProducts($productRepo);
$totalWeight = $cart->getTotalWeightForDisplay($productRepo);
$totalPrice = $cart->getTotalPrice($productRepo);
$ttcPrice = $cart->getTtcPrice($productRepo);
$weightPrice = $cart->getWeightPrice();
return $this->render('checkout/checkout.html.twig', [
'products' => $products,
'categories' => $categories,
//TO COPY TO EVERY ROUTE THAT DISPLAY CART
'totalQuantity' => $totalQuantity,
'totalWeight' => $totalWeight,
'weightPrice' => $weightPrice,
'maxQuantity' => $maxQuantity,
'cartProducts' => $cartProducts,
'totalPrice' => $totalPrice,
'ttcPrice' => $ttcPrice
]);
}
/**
* Route used with PaymentDataManager.js
* Get billing details and store data inside billing-detailes session
*
* @param Request $request
* @param ProductRepository $productRepo
* @return JsonResponse
*/
#[Route('/billing-details', name: 'billing_details')]
public function billingDetails(Request $request, ProductRepository $productRepo): JsonResponse
{
$data = $request->getContent();
$data = json_decode($data, true);
try
{
foreach ($data as $key => $value) {
// Vérification si la donnée est vide
if (empty($value) && $value !== '0') {
throw new \InvalidArgumentException("La valeur pour $key ne peut pas être vide.");
}
if ($key === 'firstname') {
// Vérification prénom : alphabétique, espace, tiret et longueur entre 1 et 50 caractères
if (!preg_match('/^[a-zA-Zà-ÿÀ-ÿ\s\-]+$/', $value) || strlen($value) < 1 || strlen($value) > 50) {
throw new \InvalidArgumentException("Le prénom ($value) n'est pas valide. Il doit contenir uniquement des lettres, des espaces et des tirets, et avoir entre 1 et 50 caractères.");
}
}
if ($key === 'lastname') {
// Vérification nom : alphabétique, espace, tiret et longueur entre 1 et 50 caractères
if (!preg_match('/^[a-zA-Zà-ÿÀ-ÿ\s\-]+$/', $value) || strlen($value) < 1 || strlen($value) > 50) {
throw new \InvalidArgumentException("Le nom ($value) n'est pas valide. Il doit contenir uniquement des lettres, des espaces et des tirets, et avoir entre 1 et 50 caractères.");
}
}
if ($key === 'address') {
// Vérification adresse : autorise lettres, chiffres, espaces, virgules, tirets, points et caractères accentués.
// Limite de longueur à 255 caractères (longueur raisonnable pour une adresse)
if (!preg_match('/^[a-zA-Z0-9à-ÿÀ-ÿ\s\-,\.#()]+$/', $value) || strlen($value) > 255) {
throw new \InvalidArgumentException("L'adresse ($value) n'est pas valide. Elle doit contenir des lettres, chiffres, espaces, virgules, tirets, points et avoir moins de 255 caractères.");
}
}
if ($key === 'additional-address-information') {
// Vérification informations additionnelles : autorise lettres, chiffres, espaces, tirets, points, slashes, # et ne dépasse pas 100 caractères
if (!preg_match('/^[a-zA-Z0-9à-ÿÀ-ÿ\s\-\.\,\/#]*$/', $value) || strlen($value) > 100) {
throw new \InvalidArgumentException("Les informations additionnelles ($value) ne sont pas valides. Elles doivent contenir des lettres, chiffres, espaces, tirets, points, slashes, # et ne pas dépasser 100 caractères.");
}
}
if ($key === 'zip') {
if (!preg_match('/^\d{5}$|^\d{5}[-\s]?\d{4}$|^[A-Za-z]{1,2}\d{1,2}[A-Za-z]?\s?\d[A-Za-z]{2}$/', $value)) {
throw new \InvalidArgumentException("Le code postal ($value) n'est pas valide. Il doit correspondre à un format valide français ou international.");
}
}
if ($key === 'phone') {
if ($value !== null && !preg_match('/^(\+?[0-9]{1,3})?[\s.-]?[0-9]{1,4}[\s.-]?[0-9]{1,4}[\s.-]?[0-9]{1,4}$/', $value)) {
throw new \InvalidArgumentException("Le téléphone ($value) n'est pas valide. Il doit être un numéro valide, international ou local.");
}
}
if ($key === 'email') {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("L'email ($value) n'est pas valide. Il doit être une adresse email correcte.");
}
}
if ($key === 'additional-delivery-information') {
// La taille maximale de 500 caractères est raisonnable pour un champ textarea
// Autorisation des lettres, chiffres, espaces, ponctuation, retours à la ligne
if (strlen($value) > 500) {
throw new \InvalidArgumentException("Les informations supplémentaires ne doivent pas dépasser 500 caractères.");
}
// Si des caractères spéciaux sont utilisés dans le texte (mais sans risque de script, etc)
if (!preg_match('/^[a-zA-Z0-9\s,.\-!?\n]*$/', $value)) {
throw new \InvalidArgumentException("Les informations supplémentaires contiennent des caractères invalides. Seuls les lettres, chiffres, espaces, virgules, points, tirets, points d'exclamation, points d'interrogation et retours à la ligne sont autorisés.");
}
}
}
//Save inside session billingDetails
$session = $request->getSession();
$session->set('billing-details', $data);
return new JsonResponse([
'message' => 'Données reçues avec succès',
'receivedData' => $data
]);
}
catch(\InvalidArgumentException $e)
{
return new JsonResponse([
'message' => $e->getMessage()
], 400);
}
}
#[Route('/payment', name: 'payment')]
public function payment(Request $request, ProductRepository $productRepo, CategoryRepository $catRepo): Response
{
$products = $productRepo->findAll();
$categories = $catRepo->findAll();
//TO COPY TO EVERY ROUTE THAT DISPLAY CART
$session = $request->getSession();
$cart = new Cart($session);
$totalQuantity = $cart->getCartTotalQuantity();
$maxQuantity = $cart->getMaxQuantityAllowed();
$cartProducts = $cart->getCartProducts($productRepo);
$totalWeight = $cart->getTotalWeightForDisplay($productRepo);
$totalPrice = $cart->getTotalPrice($productRepo);
$ttcPrice = $cart->getTtcPrice($productRepo);
$weightPrice = $cart->getWeightPrice();
//Billing details
$session = $request->getSession();
$billingDetailsSession = $session->get('billing-details');
if (!$billingDetailsSession)
{
$response = $this->redirectToRoute('app_home');
$response->headers->set('Cache-Control',
'no-store, no-cache, must-revalidate, max-age=0');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', '0');
return $response;
}
$cartSession = new Cart($session);
$ttcPrice = $cartSession->getTtcPrice($productRepo);
$this->paypalService->setTtcPrice($ttcPrice);
$response = $this->render('checkout/payment.html.twig', [
'products' => $products,
'categories' => $categories,
'billingDetails' => $billingDetailsSession,
'ttcPrice' => $ttcPrice,
//TO COPY TO EVERY ROUTE THAT DISPLAY CART
'totalQuantity' => $totalQuantity,
'totalWeight' => $totalWeight,
'weightPrice' => $weightPrice,
'maxQuantity' => $maxQuantity,
'cartProducts' => $cartProducts,
'totalPrice' => $totalPrice,
'ttcPrice' => $ttcPrice,
'paypalTest' => 'No testing ...'
]);
$response->headers->set('Cache-Control',
'no-store, no-cache, must-revalidate, max-age=0');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', '0');
return $response;
}
#[Route('/api/orders', name:'paypal_create_orders')]
public function createOrder(Request $request, ProductRepository $productRepo): JsonResponse
{
$data = json_decode($request->getContent(), true);
$session = $request->getSession();
$cartSession = new Cart($session);
$ttcPrice = $cartSession->getTtcPrice($productRepo);
$cart = $data["cart"];
$cart['ttcPrice'] = $ttcPrice;
$orderResponse = $this->paypalService->createOrder($cart);
if (isset($orderResponse['error'])) {
return new JsonResponse(['error' => $orderResponse['error']], 500);
}
return new JsonResponse($orderResponse['jsonResponse']);
}
#[Route('/api/orders/{orderId}/capture', name:'paypal_capture', methods: ['POST'])]
public function createCapture(Request $request, $orderId, ProductRepository $productRepo, EntityManagerInterface $em): JsonResponse
{
//Check product quantity stock available
$session = $request->getSession();
$cart = new Cart($session);
$cartProducts = $cart->getCartProducts($productRepo);
$cartProductsLength = count($cartProducts);
for ($i = 0; $i < $cartProductsLength ; $i++)
{
$productId = $cartProducts[$i]["product"]->getId();
$productName = $cartProducts[$i]["product"]->getName();
$productQuantityAsked = $cartProducts[$i]["quantity"];
$productStockData = $this->forward('App\Controller\MainController::getProductStock', [
'id' => $productId,
]);
$productStock = intval( json_decode($productStockData->getContent(), true));
try
{
if ($productQuantityAsked > $productStock)
{
throw new Exception('Le produit '.$productName.'n\'est plus disponible');
}
}
catch(Exception $e)
{
return new JsonResponse(["error" => $e->getMessage()]);
}
//REMOVE PRODUCT ASKED QUANTITY TO PRODUCT STOCK ENTITY
$product = $productRepo->find($productId);
$updatedProductStock = $productStock - $productQuantityAsked;
$product->setStock($updatedProductStock);
$em->persist($product);
$em->flush();
}
try {
$captureResponse = $this->paypalService->captureOrder($orderId);
$session = $request->getSession();
$billingDetails = $session->get('billing-details');
$billingDetails['captureResponse'] = $captureResponse;
$session->set('billing-details', $billingDetails);
return new JsonResponse($captureResponse["jsonResponse"]);
} catch (Exception $e) {
return new JsonResponse(["error" => $e->getMessage()]);
}
}
#[Route('/payment-sucess', name:'payment_sucess')]
public function paymentSucessMessage(Request $request, MailerInterface $mailer, ProductRepository $productRepo, CategoryRepository $catRepo)
{
$categories = $catRepo->findAll();
$session = $request->getSession();
$cart = new Cart($session);
$user = $this->getUser();
$billingDetails = $session->get('billing-details');
if (!$billingDetails)
{
return $this->redirectToRoute('app_home');
}
$transactionId = $billingDetails['captureResponse']['jsonResponse']['purchase_units'][0]['payments']['captures'][0]['id'];
if ($cart->sendEndTransactionMail($user, $mailer, $billingDetails, $productRepo))
{
$statusMessage = "N° de commande: ".$transactionId.".";//." Mail correctement envoyé";
}
else
{
$statusMessage = "Erreur, échec de la transaction, veuillez vérifier vos informations de paiement.";
}
$session->remove('cart');
$session->remove('billing-details');
$totalQuantity = $cart->getCartTotalQuantity();
$maxQuantity = $cart->getMaxQuantityAllowed();
$cartProducts = $cart->getCartProducts($productRepo);
$totalPrice = $cart->getTotalPrice($productRepo);
return $this->render('checkout/success.html.twig', [
'categories' => $categories,
'totalQuantity' => $totalQuantity,
'maxQuantity' => $maxQuantity,
'cartProducts' => $cartProducts,
'totalPrice' => $totalPrice,
'statusMessage' => $statusMessage
]);
}
}