src/Controller/ClientSpaceController.php line 50

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Contract;
  4. use App\Entity\Invoice;
  5. use App\Entity\InvoicePayment;
  6. use App\Entity\PaymentOrder;
  7. use App\Repository\ContractRepository;
  8. use App\Repository\InvoiceRepository;
  9. use App\Repository\InvoicePaymentRepository;
  10. use App\Repository\PaymentOrderRepository;
  11. use App\Repository\TaxeRepository;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\JsonResponse;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  18. use Symfony\Component\Routing\Annotation\Route;
  19. class ClientSpaceController extends AbstractController
  20. {
  21.     private $entityManager;
  22.     private $contractRepository;
  23.     private $invoiceRepository;
  24.     private $paymentRepository;
  25.     private $taxeRepository;
  26.     public function __construct(
  27.         EntityManagerInterface $entityManager,
  28.         ContractRepository $contractRepository,
  29.         InvoiceRepository $invoiceRepository,
  30.         InvoicePaymentRepository $paymentRepository,
  31.         TaxeRepository $taxeRepository
  32.     ) {
  33.         $this->entityManager $entityManager;
  34.         $this->contractRepository $contractRepository;
  35.         $this->invoiceRepository $invoiceRepository;
  36.         $this->paymentRepository $paymentRepository;
  37.         $this->taxeRepository $taxeRepository;
  38.     }
  39.     /**
  40.      * @Route("/client/login", name="client_login", methods={"GET", "POST"})
  41.      */
  42.     public function login(Request $requestSessionInterface $session): Response
  43.     {
  44.         // Si déjà connecté, rediriger vers le dashboard
  45.         if ($session->get('client_contract_id')) {
  46.             return $this->redirectToRoute('client_dashboard');
  47.         }
  48.         $error null;
  49.         if ($request->isMethod('POST')) {
  50.             $contractNumber $request->request->get('contract_number');
  51.             // Rechercher le contrat par son numéro
  52.             $contract $this->contractRepository->findOneBy(['contractNumber' => $contractNumber]);
  53.             if ($contract && $contract->isActive()) {
  54.                 // Stocker l'ID du contrat en session
  55.                 $session->set('client_contract_id'$contract->getId());
  56.                 $session->set('client_contract_number'$contract->getContractNumber());
  57.                 $session->set('client_customer_name'$contract->getCustomer()->getName());
  58.                 return $this->redirectToRoute('client_dashboard');
  59.             } else {
  60.                 $error 'Numéro de contrat invalide ou contrat inactif.';
  61.             }
  62.         }
  63.         return $this->render('client_space/login.html.twig', [
  64.             'error' => $error
  65.         ]);
  66.     }
  67.     /**
  68.      * @Route("/client/dashboard", name="client_dashboard", methods={"GET"})
  69.      */
  70.     public function dashboard(SessionInterface $session): Response
  71.     {
  72.         // Vérifier si le client est connecté
  73.         $contractId $session->get('client_contract_id');
  74.         if (!$contractId) {
  75.             return $this->redirectToRoute('client_login');
  76.         }
  77.         // Récupérer le contrat de connexion
  78.         $contract $this->contractRepository->find($contractId);
  79.         if (!$contract) {
  80.             $session->clear();
  81.             return $this->redirectToRoute('client_login');
  82.         }
  83.         $customer $contract->getCustomer();
  84.         // Récupérer TOUS les contrats du client
  85.         $allContracts $this->contractRepository->findBy(['customer' => $customer]);
  86.         // Récupérer TOUTES les factures de TOUS les contrats du client
  87.         $allInvoices $this->entityManager->createQueryBuilder()
  88.             ->select('i')
  89.             ->from('App\Entity\Invoice''i')
  90.             ->join('i.contract''c')
  91.             ->where('c.customer = :customer')
  92.             ->setParameter('customer'$customer)
  93.             ->orderBy('i.issueDate''DESC')
  94.             ->getQuery()
  95.             ->getResult();
  96.         // Récupérer les taxes applicables pour le client
  97.         $applicableTaxes $this->taxeRepository->findApplicableTaxesForCustomer($customer);
  98.         // Calculer les statistiques globales
  99.         $totalInvoices count($allInvoices);
  100.         $totalAmount 0;
  101.         $totalPaid 0;
  102.         $totalUnpaid 0;
  103.         $overdueCount 0;
  104.         $now = new \DateTime();
  105.         foreach ($allInvoices as $invoice) {
  106.             $totalAmount += $invoice->getTotalAmount();
  107.             $paidAmount $invoice->getPaidAmount();
  108.             $totalPaid += $paidAmount;
  109.             
  110.             // Calculer le montant restant basé sur la part SNEL uniquement
  111.             $snelAmount $invoice->getSnelAmount($applicableTaxes);
  112.             $remaining $snelAmount $paidAmount;
  113.             $totalUnpaid += max(0$remaining);
  114.             // Vérifier si la facture est en retard
  115.             if ($remaining && $invoice->getDueDate() < $now) {
  116.                 $overdueCount++;
  117.             }
  118.         }
  119.         // Récupérer les 5 dernières factures
  120.         $recentInvoices array_slice($allInvoices05);
  121.         // Récupérer les derniers paiements validés de tous les contrats
  122.         $recentPayments $this->entityManager->createQueryBuilder()
  123.             ->select('p''i')
  124.             ->from('App\Entity\InvoicePayment''p')
  125.             ->join('p.invoice''i')
  126.             ->join('i.contract''c')
  127.             ->where('c.customer = :customer')
  128.             ->andWhere('p.validated = :validated')
  129.             ->andWhere('p.paymentDate IS NOT NULL')
  130.             ->andWhere('p.deletedAt IS NULL')
  131.             ->setParameter('customer'$customer)
  132.             ->setParameter('validated'true)
  133.             ->orderBy('p.paymentDate''DESC')
  134.             ->setMaxResults(3)
  135.             ->getQuery()
  136.             ->getResult();
  137.         // Récupérer les derniers bons de perception du client
  138.         $recentPaymentOrders $this->entityManager->createQueryBuilder()
  139.             ->select('po')
  140.             ->from('App\Entity\PaymentOrder''po')
  141.             ->join('po.invoicePayments''ip')
  142.             ->join('ip.invoice''i')
  143.             ->join('i.contract''c')
  144.             ->where('c.customer = :customer')
  145.             ->setParameter('customer'$customer)
  146.             ->groupBy('po.id')
  147.             ->orderBy('po.createdAt''DESC')
  148.             ->setMaxResults(3)
  149.             ->getQuery()
  150.             ->getResult();
  151.         return $this->render('client_space/dashboard.html.twig', [
  152.             'contract' => $contract,
  153.             'customer' => $customer,
  154.             'allContracts' => $allContracts,
  155.             'totalContracts' => count($allContracts),
  156.             'totalInvoices' => $totalInvoices,
  157.             'totalAmount' => $totalAmount,
  158.             'totalPaid' => $totalPaid,
  159.             'totalUnpaid' => $totalUnpaid,
  160.             'overdueCount' => $overdueCount,
  161.             'recentInvoices' => $recentInvoices,
  162.             'recentPayments' => $recentPayments,
  163.             'recentPaymentOrders' => $recentPaymentOrders,
  164.             'applicableTaxes' => $applicableTaxes,
  165.         ]);
  166.     }
  167.     /**
  168.      * @Route("/client/invoices", name="client_invoices", methods={"GET"})
  169.      */
  170.     public function invoices(SessionInterface $session): Response
  171.     {
  172.         $contractId $session->get('client_contract_id');
  173.         if (!$contractId) {
  174.             return $this->redirectToRoute('client_login');
  175.         }
  176.         $contract $this->contractRepository->find($contractId);
  177.         if (!$contract) {
  178.             $session->clear();
  179.             return $this->redirectToRoute('client_login');
  180.         }
  181.         $customer $contract->getCustomer();
  182.         // Récupérer TOUTES les factures de TOUS les contrats du client
  183.         $allInvoices $this->entityManager->createQueryBuilder()
  184.             ->select('i''c')
  185.             ->from('App\Entity\Invoice''i')
  186.             ->join('i.contract''c')
  187.             ->where('c.customer = :customer')
  188.             ->setParameter('customer'$customer)
  189.             ->orderBy('i.period''DESC')
  190.             ->addOrderBy('i.issueDate''DESC')
  191.             ->getQuery()
  192.             ->getResult();
  193.         // Regrouper les factures par période
  194.         $invoicesByPeriod = [];
  195.         foreach ($allInvoices as $invoice) {
  196.             $period $invoice->getPeriod();
  197.             if (!isset($invoicesByPeriod[$period])) {
  198.                 $invoicesByPeriod[$period] = [
  199.                     'invoices' => [],
  200.                     'totalAmount' => 0,
  201.                     'totalPaid' => 0,
  202.                     'totalRemaining' => 0,
  203.                     'allPaid' => true,
  204.                     'hasOverdue' => false,
  205.                 ];
  206.             }
  207.             
  208.             $paidAmount $invoice->getPaidAmount();
  209.             $remaining $invoice->getTotalAmount() - $paidAmount;
  210.             
  211.             $invoicesByPeriod[$period]['invoices'][] = $invoice;
  212.             $invoicesByPeriod[$period]['totalAmount'] += $invoice->getTotalAmount();
  213.             $invoicesByPeriod[$period]['totalPaid'] += $paidAmount;
  214.             $invoicesByPeriod[$period]['totalRemaining'] += $remaining;
  215.             
  216.             if ($remaining 0) {
  217.                 $invoicesByPeriod[$period]['allPaid'] = false;
  218.                 if ($invoice->getDueDate() < new \DateTime()) {
  219.                     $invoicesByPeriod[$period]['hasOverdue'] = true;
  220.                 }
  221.             }
  222.         }
  223.         // Calculer les taxes applicables au client
  224.         $applicableTaxes $this->taxeRepository->findApplicableTaxesForCustomer($customer);
  225.         // Récupérer les moyens de paiement actifs
  226.         $paymentGateways $this->entityManager->getRepository('App\Entity\PaymentGateway')
  227.             ->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
  228.         return $this->render('client_space/invoices.html.twig', [
  229.             'contract' => $contract,
  230.             'customer' => $customer,
  231.             'invoicesByPeriod' => $invoicesByPeriod,
  232.             'paymentGateways' => $paymentGateways,
  233.             'applicableTaxes' => $applicableTaxes,
  234.         ]);
  235.     }
  236.     /**
  237.      * @Route("/client/invoices/{id}/taxes-detail", name="client_invoice_taxes_detail", methods={"GET"})
  238.      */
  239.     public function invoiceTaxesDetail(int $idSessionInterface $session): JsonResponse
  240.     {
  241.         $contractId $session->get('client_contract_id');
  242.         if (!$contractId) {
  243.             return new JsonResponse(['success' => false'message' => 'Non authentifié'], 401);
  244.         }
  245.         $contract $this->contractRepository->find($contractId);
  246.         if (!$contract) {
  247.             return new JsonResponse(['success' => false'message' => 'Contrat introuvable'], 404);
  248.         }
  249.         $invoice $this->invoiceRepository->find($id);
  250.         if (!$invoice) {
  251.             return new JsonResponse(['success' => false'message' => 'Facture introuvable'], 404);
  252.         }
  253.         // Vérifier que la facture appartient bien au client
  254.         if ($invoice->getContract()->getCustomer()->getId() !== $contract->getCustomer()->getId()) {
  255.             return new JsonResponse(['success' => false'message' => 'Accès non autorisé'], 403);
  256.         }
  257.         // Récupérer les taxes depuis le JSON taxesBreakdown
  258.         $taxesBreakdown $invoice->getTaxesBreakdown();
  259.         $taxes = [];
  260.         $totalTaxAmount 0;
  261.         if ($taxesBreakdown && isset($taxesBreakdown['taxes'])) {
  262.             foreach ($taxesBreakdown['taxes'] as $tax) {
  263.                 $taxes[] = [
  264.                     'name' => $tax['name'],
  265.                     'percentage' => $tax['percentage'],
  266.                     'amount' => $tax['amount']
  267.                 ];
  268.                 $totalTaxAmount += $tax['amount'];
  269.             }
  270.         }
  271.         return new JsonResponse([
  272.             'success' => true,
  273.             'invoiceNumber' => $invoice->getInvoiceNumber(),
  274.             'taxes' => $taxes,
  275.             'totalTaxAmount' => $totalTaxAmount,
  276.             'ttcAmount' => $invoice->getTotalAmount()
  277.         ]);
  278.     }
  279.     /**
  280.      * @Route("/client/payments-history", name="client_payments_history", methods={"GET"})
  281.      */
  282.     public function paymentsHistory(SessionInterface $session): Response
  283.     {
  284.         $contractId $session->get('client_contract_id');
  285.         if (!$contractId) {
  286.             return $this->redirectToRoute('client_login');
  287.         }
  288.         $contract $this->contractRepository->find($contractId);
  289.         if (!$contract) {
  290.             $session->clear();
  291.             return $this->redirectToRoute('client_login');
  292.         }
  293.         $customer $contract->getCustomer();
  294.         // Récupérer les paiements validés de TOUS les contrats du client
  295.         // Un paiement est considéré comme validé si:
  296.         // - validated = true (paiement approuvé)
  297.         // - paymentDate existe (paiement effectué)
  298.         // - deletedAt est null (non supprimé)
  299.         $payments $this->entityManager->createQueryBuilder()
  300.             ->select('p''i''c')
  301.             ->from('App\Entity\InvoicePayment''p')
  302.             ->join('p.invoice''i')
  303.             ->join('i.contract''c')
  304.             ->where('c.customer = :customer')
  305.             ->andWhere('p.validated = :validated')
  306.             ->andWhere('p.paymentDate IS NOT NULL')
  307.             ->andWhere('p.deletedAt IS NULL')
  308.             ->setParameter('customer'$customer)
  309.             ->setParameter('validated'true)
  310.             ->orderBy('p.paymentDate''DESC')
  311.             ->getQuery()
  312.             ->getResult();
  313.         // Récupérer TOUTES les factures de TOUS les contrats du client
  314.         $allInvoices $this->entityManager->createQueryBuilder()
  315.             ->select('i''c')
  316.             ->from('App\Entity\Invoice''i')
  317.             ->join('i.contract''c')
  318.             ->where('c.customer = :customer')
  319.             ->setParameter('customer'$customer)
  320.             ->orderBy('i.issueDate''DESC')
  321.             ->getQuery()
  322.             ->getResult();
  323.         // Récupérer toutes les périodes uniques pour le filtre
  324.         $periods $this->entityManager->createQueryBuilder()
  325.             ->select('DISTINCT i.period')
  326.             ->from('App\Entity\Invoice''i')
  327.             ->join('i.contract''c')
  328.             ->where('c.customer = :customer')
  329.             ->setParameter('customer'$customer)
  330.             ->orderBy('i.period''DESC')
  331.             ->getQuery()
  332.             ->getResult();
  333.         // Récupérer tous les numéros de factures uniques pour le filtre
  334.         $invoiceNumbers $this->entityManager->createQueryBuilder()
  335.             ->select('DISTINCT i.invoiceNumber')
  336.             ->from('App\Entity\Invoice''i')
  337.             ->join('i.contract''c')
  338.             ->where('c.customer = :customer')
  339.             ->setParameter('customer'$customer)
  340.             ->orderBy('i.invoiceNumber''DESC')
  341.             ->getQuery()
  342.             ->getResult();
  343.         // Préparer les données pour le graphique (sérialisation simplifiée)
  344.         $paymentsForChart = [];
  345.         foreach ($payments as $payment) {
  346.             $paymentsForChart[] = [
  347.                 'amount' => (float) $payment->getAmount(),
  348.                 'paymentDate' => $payment->getPaymentDate() ? $payment->getPaymentDate()->format('Y-m-d') : null,
  349.             ];
  350.         }
  351.         return $this->render('client_space/payments_history.html.twig', [
  352.             'contract' => $contract,
  353.             'customer' => $customer,
  354.             'payments' => $payments,
  355.             'paymentsForChart' => $paymentsForChart,
  356.             'allInvoices' => $allInvoices,
  357.             'periods' => array_column($periods'period'),
  358.             'invoiceNumbers' => array_column($invoiceNumbers'invoiceNumber'),
  359.         ]);
  360.     }
  361.     /**
  362.      * @Route("/client/invoice/{id}", name="client_invoice_detail", methods={"GET"})
  363.      */
  364.     public function invoiceDetail(int $idSessionInterface $session): Response
  365.     {
  366.         $contractId $session->get('client_contract_id');
  367.         if (!$contractId) {
  368.             return $this->redirectToRoute('client_login');
  369.         }
  370.         $contract $this->contractRepository->find($contractId);
  371.         if (!$contract) {
  372.             $session->clear();
  373.             return $this->redirectToRoute('client_login');
  374.         }
  375.         $customer $contract->getCustomer();
  376.         // Récupérer la facture et vérifier qu'elle appartient au client
  377.         $invoice $this->entityManager->createQueryBuilder()
  378.             ->select('i''c')
  379.             ->from('App\Entity\Invoice''i')
  380.             ->join('i.contract''c')
  381.             ->where('i.id = :id')
  382.             ->andWhere('c.customer = :customer')
  383.             ->setParameter('id'$id)
  384.             ->setParameter('customer'$customer)
  385.             ->getQuery()
  386.             ->getOneOrNullResult();
  387.         if (!$invoice) {
  388.             throw $this->createNotFoundException('Facture non trouvée');
  389.         }
  390.         // Récupérer les taxes applicables
  391.         $applicableTaxes $this->taxeRepository->findApplicableTaxesForCustomer($customer);
  392.         // Récupérer les paiements de cette facture
  393.         $payments $this->entityManager->createQueryBuilder()
  394.             ->select('p')
  395.             ->from('App\Entity\InvoicePayment''p')
  396.             ->where('p.invoice = :invoice')
  397.             ->andWhere('p.paymentDate IS NOT NULL')
  398.             ->andWhere('p.deletedAt IS NULL')
  399.             ->andWhere('(p.validated = :validated OR p.validated IS NULL)')
  400.             ->setParameter('invoice'$invoice)
  401.             ->setParameter('validated'true)
  402.             ->orderBy('p.paymentDate''DESC')
  403.             ->getQuery()
  404.             ->getResult();
  405.         return $this->render('client_space/invoice_detail.html.twig', [
  406.             'contract' => $contract,
  407.             'customer' => $customer,
  408.             'invoice' => $invoice,
  409.             'applicableTaxes' => $applicableTaxes,
  410.             'payments' => $payments,
  411.         ]);
  412.     }
  413.     /**
  414.      * @Route("/client/contract-info", name="client_contract_info", methods={"GET"})
  415.      */
  416.     public function contractInfo(SessionInterface $session): Response
  417.     {
  418.         $contractId $session->get('client_contract_id');
  419.         if (!$contractId) {
  420.             return $this->redirectToRoute('client_login');
  421.         }
  422.         $contract $this->contractRepository->find($contractId);
  423.         if (!$contract) {
  424.             $session->clear();
  425.             return $this->redirectToRoute('client_login');
  426.         }
  427.         $customer $contract->getCustomer();
  428.         // Récupérer tous les contrats du client
  429.         $allContracts $this->contractRepository->findBy(
  430.             ['customer' => $customer],
  431.             ['createdAt' => 'DESC']
  432.         );
  433.         // Préparer les statistiques pour chaque contrat
  434.         $contractsData = [];
  435.         foreach ($allContracts as $c) {
  436.             $invoices $c->getInvoices();
  437.             $totalInvoices count($invoices);
  438.             $paidInvoices 0;
  439.             $partiallyPaidInvoices 0;
  440.             $unpaidInvoices 0;
  441.             $totalAmount 0;
  442.             $paidAmount 0;
  443.             foreach ($invoices as $invoice) {
  444.                 $totalAmount += $invoice->getTotalAmount();
  445.                 $paidAmount += $invoice->getPaidAmount();
  446.                 
  447.                 if ($invoice->isFullyPaid()) {
  448.                     $paidInvoices++;
  449.                 } elseif ($invoice->getPaidAmount() > 0) {
  450.                     $partiallyPaidInvoices++;
  451.                 } else {
  452.                     $unpaidInvoices++;
  453.                 }
  454.             }
  455.             $contractsData[] = [
  456.                 'contract' => $c,
  457.                 'totalInvoices' => $totalInvoices,
  458.                 'paidInvoices' => $paidInvoices,
  459.                 'partiallyPaidInvoices' => $partiallyPaidInvoices,
  460.                 'unpaidInvoices' => $unpaidInvoices,
  461.                 'totalAmount' => $totalAmount,
  462.                 'paidAmount' => $paidAmount,
  463.                 'remainingAmount' => $totalAmount $paidAmount,
  464.             ];
  465.         }
  466.         return $this->render('client_space/contract_info.html.twig', [
  467.             'contract' => $contract,
  468.             'customer' => $customer,
  469.             'contractsData' => $contractsData,
  470.         ]);
  471.     }
  472.     /**
  473.      * @Route("/client/contract/{id}/invoices", name="client_contract_invoices", methods={"GET"})
  474.      */
  475.     public function contractInvoices(int $idSessionInterface $session): Response
  476.     {
  477.         $contractId $session->get('client_contract_id');
  478.         if (!$contractId) {
  479.             return $this->redirectToRoute('client_login');
  480.         }
  481.         $currentContract $this->contractRepository->find($contractId);
  482.         if (!$currentContract) {
  483.             $session->clear();
  484.             return $this->redirectToRoute('client_login');
  485.         }
  486.         // Récupérer le contrat demandé
  487.         $contract $this->contractRepository->find($id);
  488.         if (!$contract) {
  489.             throw $this->createNotFoundException('Contrat introuvable');
  490.         }
  491.         // Vérifier que le contrat appartient bien au client
  492.         if ($contract->getCustomer()->getId() !== $currentContract->getCustomer()->getId()) {
  493.             throw $this->createAccessDeniedException('Accès non autorisé');
  494.         }
  495.         $customer $contract->getCustomer();
  496.         // Récupérer toutes les factures de ce contrat
  497.         $invoices $this->entityManager->createQueryBuilder()
  498.             ->select('i')
  499.             ->from('App\Entity\Invoice''i')
  500.             ->where('i.contract = :contract')
  501.             ->setParameter('contract'$contract)
  502.             ->orderBy('i.period''DESC')
  503.             ->addOrderBy('i.issueDate''DESC')
  504.             ->getQuery()
  505.             ->getResult();
  506.         // Regrouper les factures par période
  507.         $invoicesByPeriod = [];
  508.         foreach ($invoices as $invoice) {
  509.             $period $invoice->getPeriod();
  510.             if (!isset($invoicesByPeriod[$period])) {
  511.                 $invoicesByPeriod[$period] = [
  512.                     'invoices' => [],
  513.                     'totalAmount' => 0,
  514.                     'totalPaid' => 0,
  515.                     'totalRemaining' => 0,
  516.                     'allPaid' => true,
  517.                     'hasOverdue' => false,
  518.                 ];
  519.             }
  520.             
  521.             $paidAmount $invoice->getPaidAmount();
  522.             $remaining $invoice->getTotalAmount() - $paidAmount;
  523.             
  524.             $invoicesByPeriod[$period]['invoices'][] = $invoice;
  525.             $invoicesByPeriod[$period]['totalAmount'] += $invoice->getTotalAmount();
  526.             $invoicesByPeriod[$period]['totalPaid'] += $paidAmount;
  527.             $invoicesByPeriod[$period]['totalRemaining'] += $remaining;
  528.             
  529.             if ($remaining 0) {
  530.                 $invoicesByPeriod[$period]['allPaid'] = false;
  531.                 if ($invoice->getDueDate() < new \DateTime()) {
  532.                     $invoicesByPeriod[$period]['hasOverdue'] = true;
  533.                 }
  534.             }
  535.         }
  536.         // Calculer les taxes applicables au client
  537.         $applicableTaxes $this->taxeRepository->findApplicableTaxesForCustomer($customer);
  538.         // Récupérer les moyens de paiement actifs
  539.         $paymentGateways $this->entityManager->getRepository('App\Entity\PaymentGateway')
  540.             ->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
  541.         return $this->render('client_space/contract_invoices.html.twig', [
  542.             'contract' => $contract,
  543.             'customer' => $customer,
  544.             'invoicesByPeriod' => $invoicesByPeriod,
  545.             'paymentGateways' => $paymentGateways,
  546.             'applicableTaxes' => $applicableTaxes,
  547.         ]);
  548.     }
  549.     /**
  550.      * @Route("/client/payment-orders", name="client_payment_orders", methods={"GET"})
  551.      */
  552.     public function paymentOrders(SessionInterface $session): Response
  553.     {
  554.         $contractId $session->get('client_contract_id');
  555.         if (!$contractId) {
  556.             return $this->redirectToRoute('client_login');
  557.         }
  558.         $contract $this->contractRepository->find($contractId);
  559.         if (!$contract) {
  560.             $session->clear();
  561.             return $this->redirectToRoute('client_login');
  562.         }
  563.         $customer $contract->getCustomer();
  564.         // Récupérer tous les bons de perception du client
  565.         $paymentOrders $this->entityManager->createQueryBuilder()
  566.             ->select('po')
  567.             ->from('App\Entity\PaymentOrder''po')
  568.             ->join('po.invoicePayments''ip')
  569.             ->join('ip.invoice''i')
  570.             ->join('i.contract''c')
  571.             ->where('c.customer = :customer')
  572.             ->setParameter('customer'$customer)
  573.             ->groupBy('po.id')
  574.             ->orderBy('po.createdAt''DESC')
  575.             ->getQuery()
  576.             ->getResult();
  577.         return $this->render('client_space/payment_orders.html.twig', [
  578.             'contract' => $contract,
  579.             'customer' => $customer,
  580.             'paymentOrders' => $paymentOrders,
  581.         ]);
  582.     }
  583.     /**
  584.      * @Route("/client/payment-order/{id}", name="client_payment_order_detail", methods={"GET"})
  585.      */
  586.     public function paymentOrderDetail(int $idSessionInterface $session): Response
  587.     {
  588.         $contractId $session->get('client_contract_id');
  589.         if (!$contractId) {
  590.             return $this->redirectToRoute('client_login');
  591.         }
  592.         $contract $this->contractRepository->find($contractId);
  593.         if (!$contract) {
  594.             $session->clear();
  595.             return $this->redirectToRoute('client_login');
  596.         }
  597.         $customer $contract->getCustomer();
  598.         // Récupérer le bon de perception avec vérification d'accès
  599.         $paymentOrder $this->entityManager->createQueryBuilder()
  600.             ->select('po''ip''i''c')
  601.             ->from('App\Entity\PaymentOrder''po')
  602.             ->join('po.invoicePayments''ip')
  603.             ->join('ip.invoice''i')
  604.             ->join('i.contract''c')
  605.             ->where('po.id = :id')
  606.             ->andWhere('c.customer = :customer')
  607.             ->setParameter('id'$id)
  608.             ->setParameter('customer'$customer)
  609.             ->getQuery()
  610.             ->getOneOrNullResult();
  611.         if (!$paymentOrder) {
  612.             throw $this->createNotFoundException('Bon de perception non trouvé');
  613.         }
  614.         // Récupérer les moyens de paiement en ligne actifs
  615.         $paymentGateways $this->entityManager->getRepository('App\Entity\PaymentGateway')
  616.             ->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
  617.         // Récupérer les comptes actifs et disponibles pour paiement
  618.         $accounts $this->entityManager->getRepository('App\Entity\Account')
  619.             ->findBy([
  620.                 'isActive' => true,
  621.                 'isAvailableForPayment' => true
  622.             ], ['accountNumber' => 'ASC']);
  623.         // Calculer le montant SNEL total et les taxes totales
  624.         $totalSnelAmount 0;
  625.         $totalTaxAmount 0;
  626.         $applicableTaxes $this->taxeRepository->findApplicableTaxesForCustomer($customer);
  627.         
  628.         foreach ($paymentOrder->getInvoicePayments() as $invoicePayment) {
  629.             $invoice $invoicePayment->getInvoice();
  630.             $totalSnelAmount += $invoice->getSnelAmount($applicableTaxes);
  631.             $totalTaxAmount += $invoice->getTaxAmount($applicableTaxes);
  632.         }
  633.         return $this->render('client_space/payment_order_detail.html.twig', [
  634.             'contract' => $contract,
  635.             'customer' => $customer,
  636.             'paymentGateways' => $paymentGateways,
  637.             'paymentOrder' => $paymentOrder,
  638.             'accounts' => $accounts,
  639.             'totalSnelAmount' => $totalSnelAmount,
  640.             'totalTaxAmount' => $totalTaxAmount,
  641.         ]);
  642.     }
  643.     /**
  644.      * @Route("/client/payment-order/{id}/upload-proof", name="client_upload_proof_of_payment", methods={"POST"})
  645.      */
  646.     public function uploadProofOfPayment(int $idRequest $requestSessionInterface $session): JsonResponse
  647.     {
  648.         $contractId $session->get('client_contract_id');
  649.         if (!$contractId) {
  650.             return new JsonResponse(['success' => false'message' => 'Non authentifié'], 401);
  651.         }
  652.         $contract $this->contractRepository->find($contractId);
  653.         if (!$contract) {
  654.             return new JsonResponse(['success' => false'message' => 'Contrat introuvable'], 404);
  655.         }
  656.         $customer $contract->getCustomer();
  657.         // Récupérer le bon de perception avec vérification d'accès
  658.         $paymentOrder $this->entityManager->createQueryBuilder()
  659.             ->select('po''ip''i''c')
  660.             ->from('App\Entity\PaymentOrder''po')
  661.             ->join('po.invoicePayments''ip')
  662.             ->join('ip.invoice''i')
  663.             ->join('i.contract''c')
  664.             ->where('po.id = :id')
  665.             ->andWhere('c.customer = :customer')
  666.             ->setParameter('id'$id)
  667.             ->setParameter('customer'$customer)
  668.             ->getQuery()
  669.             ->getOneOrNullResult();
  670.         if (!$paymentOrder) {
  671.             return new JsonResponse(['success' => false'message' => 'Bon de perception non trouvé'], 404);
  672.         }
  673.         // Vérifier si le bon n'est pas déjà validé
  674.         if ($paymentOrder->getStatus()) {
  675.             return new JsonResponse(['success' => false'message' => 'Ce bon de perception est déjà validé'], 400);
  676.         }
  677.         // Récupérer le fichier uploadé
  678.         $file $request->files->get('proofFile');
  679.         if (!$file) {
  680.             return new JsonResponse(['success' => false'message' => 'Aucun fichier fourni'], 400);
  681.         }
  682.         // Valider le fichier
  683.         $allowedMimeTypes = ['image/jpeg''image/png''image/jpg''application/pdf'];
  684.         if (!in_array($file->getMimeType(), $allowedMimeTypes)) {
  685.             return new JsonResponse(['success' => false'message' => 'Type de fichier non autorisé. Formats acceptés: JPG, PNG, PDF'], 400);
  686.         }
  687.         if ($file->getSize() > 1024 1024) { // 5MB
  688.             return new JsonResponse(['success' => false'message' => 'Le fichier est trop volumineux. Taille maximale: 5MB'], 400);
  689.         }
  690.         try {
  691.             // Créer le répertoire s'il n'existe pas
  692.             $uploadDir $this->getParameter('kernel.project_dir') . '/public/uploads/proof_of_payment';
  693.             if (!is_dir($uploadDir)) {
  694.                 mkdir($uploadDir0777true);
  695.             }
  696.             // Générer un nom de fichier unique
  697.             $fileName uniqid() . '_' $paymentOrder->getNumAuto() . '.' $file->guessExtension();
  698.             $file->move($uploadDir$fileName);
  699.             // Mettre à jour le bon de paiement avec toutes les informations
  700.             $paymentOrder->setProofOfPayment($fileName);
  701.             
  702.             // Informations de transaction
  703.             $transactionNumber $request->request->get('transactionNumber');
  704.             if ($transactionNumber) {
  705.                 $paymentOrder->setTransactionNumber($transactionNumber);
  706.             }
  707.             
  708.             $transactionDate $request->request->get('transactionDate');
  709.             if ($transactionDate) {
  710.                 $paymentOrder->setTransactionDate(new \DateTime($transactionDate));
  711.             }
  712.             
  713.             $paymentMethod $request->request->get('paymentMethod');
  714.             if ($paymentMethod) {
  715.                 $paymentOrder->setPaymentMethod($paymentMethod);
  716.             }
  717.             
  718.             $accountNumber $request->request->get('accountNumber');
  719.             if ($accountNumber) {
  720.                 $paymentOrder->setAccountNumber($accountNumber);
  721.             }
  722.             
  723.             $agentCode $request->request->get('agentCode');
  724.             if ($agentCode) {
  725.                 $paymentOrder->setAgentCode($agentCode);
  726.             }
  727.             
  728.             $agencyCode $request->request->get('agencyCode');
  729.             if ($agencyCode) {
  730.                 $paymentOrder->setAgencyCode($agencyCode);
  731.             }
  732.             
  733.             // Ajouter les notes si fournies
  734.             $notes $request->request->get('notes');
  735.             if ($notes) {
  736.                 $existingNotes $paymentOrder->getNotes();
  737.                 $newNotes $existingNotes $existingNotes "\n\n--- Preuve de paiement ---\n" $notes $notes;
  738.                 $paymentOrder->setNotes($newNotes);
  739.             }
  740.             $this->entityManager->flush();
  741.             return new JsonResponse([
  742.                 'success' => true,
  743.                 'message' => 'Votre preuve de paiement a été envoyée avec succès. Elle sera vérifiée par nos services dans les plus brefs délais.'
  744.             ]);
  745.         } catch (\Exception $e) {
  746.             return new JsonResponse([
  747.                 'success' => false,
  748.                 'message' => 'Erreur lors de l\'upload: ' $e->getMessage()
  749.             ], 500);
  750.         }
  751.     }
  752.     /**
  753.      * @Route("/client/generate-payment-order", name="client_generate_payment_order", methods={"POST"})
  754.      */
  755.     public function generatePaymentOrder(Request $requestSessionInterface $session): JsonResponse
  756.     {
  757.         $contractId $session->get('client_contract_id');
  758.         if (!$contractId) {
  759.             return new JsonResponse(['error' => 'Non authentifié'], 401);
  760.         }
  761.         $contract $this->contractRepository->find($contractId);
  762.         if (!$contract) {
  763.             return new JsonResponse(['error' => 'Contrat introuvable'], 404);
  764.         }
  765.         $data json_decode($request->getContent(), true);
  766.         $cartItems $data['cart'] ?? [];
  767.         $paymentMethod $data['paymentMethod'] ?? 'Non spécifié';
  768.         if (empty($cartItems)) {
  769.             return new JsonResponse(['error' => 'Panier vide'], 400);
  770.         }
  771.         try {
  772.             // Créer le bon de perception
  773.             $paymentOrder = new PaymentOrder();
  774.             $paymentOrder->setPaymentMethod($paymentMethod);
  775.             
  776.             $totalAmount 0;
  777.             foreach ($cartItems as $item) {
  778.                 $invoice $this->entityManager->getRepository('App\Entity\Invoice')->find($item['invoiceId']);
  779.                 
  780.                 if (!$invoice) {
  781.                     continue;
  782.                 }
  783.                 $invoicePayment = new InvoicePayment();
  784.                 $invoicePayment->setInvoice($invoice);
  785.                 $invoicePayment->setAmount($item['amount']);
  786.                 //$invoicePayment->setPaymentDate(new \DateTime());
  787.                 //$invoicePayment->setPaymentMethod('bank_order');
  788.                 // createdBy est null pour les paiements créés depuis l'espace client
  789.                 
  790.                 $paymentOrder->addInvoicePayment($invoicePayment);
  791.                 $this->entityManager->persist($invoicePayment);
  792.                 
  793.                 $totalAmount += (float) $item['amount'];
  794.             }
  795.             $paymentOrder->setTotalAmount($totalAmount);
  796.             $this->entityManager->persist($paymentOrder);
  797.             $this->entityManager->flush();
  798.             return new JsonResponse([
  799.                 'success' => true,
  800.                 'numAuto' => $paymentOrder->getNumAuto(),
  801.                 'paymentOrderId' => $paymentOrder->getId(),
  802.                 'totalAmount' => $totalAmount,
  803.                 'message' => 'Bon de perception généré avec succès'
  804.             ]);
  805.         } catch (\Exception $e) {
  806.             return new JsonResponse(['error' => 'Erreur lors de la génération: ' $e->getMessage()], 500);
  807.         }
  808.     }
  809.     /**
  810.      * @Route("/client/download-payment-order/{id}", name="client_download_payment_order", methods={"GET"})
  811.      */
  812.     public function downloadPaymentOrder(int $idSessionInterface $session): Response
  813.     {
  814.         $contractId $session->get('client_contract_id');
  815.         if (!$contractId) {
  816.             return $this->redirectToRoute('client_login');
  817.         }
  818.         $paymentOrderRepo $this->entityManager->getRepository(PaymentOrder::class);
  819.         $paymentOrder $paymentOrderRepo->find($id);
  820.         if (!$paymentOrder) {
  821.             throw $this->createNotFoundException('Bon de perception introuvable');
  822.         }
  823.         // Vérifier que le client a accès à ce bon (via les factures)
  824.         $contract $this->contractRepository->find($contractId);
  825.         $customer $contract->getCustomer();
  826.         
  827.         // Vérifier que toutes les factures appartiennent bien au client
  828.         $hasAccess false;
  829.         foreach ($paymentOrder->getInvoicePayments() as $payment) {
  830.             if ($payment->getInvoice()->getContract()->getCustomer()->getId() === $customer->getId()) {
  831.                 $hasAccess true;
  832.                 break;
  833.             }
  834.         }
  835.         
  836.         if (!$hasAccess) {
  837.             throw $this->createNotFoundException('Bon de perception introuvable');
  838.         }
  839.         return $this->render('client_space/payment_order_pdf.html.twig', [
  840.             'paymentOrder' => $paymentOrder,
  841.             'customer' => $customer,
  842.         ]);
  843.     }
  844.     /**
  845.      * @Route("/client/download-payment-receipt/{id}", name="client_download_payment_receipt", methods={"GET"})
  846.      */
  847.     public function downloadPaymentReceipt(int $idSessionInterface $session): Response
  848.     {
  849.         $contractId $session->get('client_contract_id');
  850.         if (!$contractId) {
  851.             return $this->redirectToRoute('client_login');
  852.         }
  853.         $paymentOrderRepo $this->entityManager->getRepository(PaymentOrder::class);
  854.         $paymentOrder $paymentOrderRepo->find($id);
  855.         if (!$paymentOrder) {
  856.             throw $this->createNotFoundException('Bon de paiement introuvable');
  857.         }
  858.         // Vérifier que le bon est validé
  859.         if (!$paymentOrder->getStatus()) {
  860.             throw $this->createNotFoundException('Le reçu n\'est disponible que pour les bons validés');
  861.         }
  862.         // Vérifier que le client a accès à ce bon (via les factures)
  863.         $contract $this->contractRepository->find($contractId);
  864.         $customer $contract->getCustomer();
  865.         
  866.         // Vérifier que toutes les factures appartiennent bien au client
  867.         $hasAccess false;
  868.         foreach ($paymentOrder->getInvoicePayments() as $payment) {
  869.             if ($payment->getInvoice()->getContract()->getCustomer()->getId() === $customer->getId()) {
  870.                 $hasAccess true;
  871.                 break;
  872.             }
  873.         }
  874.         
  875.         if (!$hasAccess) {
  876.             throw $this->createNotFoundException('Bon de paiement introuvable');
  877.         }
  878.         // Générer le numéro de reçu s'il n'existe pas
  879.         if (!$paymentOrder->getReceiptNumber()) {
  880.             $receiptNumber $this->generateReceiptNumber();
  881.             $paymentOrder->setReceiptNumber($receiptNumber);
  882.             $this->entityManager->flush();
  883.         }
  884.         return $this->render('client_space/payment_receipt_pdf.html.twig', [
  885.             'paymentOrder' => $paymentOrder,
  886.             'customer' => $customer,
  887.         ]);
  888.     }
  889.     /**
  890.      * Génère un numéro de reçu unique au format REC-YYYYNNN-HASH
  891.      */
  892.     private function generateReceiptNumber(): string
  893.     {
  894.         $year date('Y');
  895.         $paymentOrderRepo $this->entityManager->getRepository(PaymentOrder::class);
  896.         
  897.         // Trouver le dernier numéro de reçu de l'année
  898.         $lastReceipt $paymentOrderRepo->createQueryBuilder('p')
  899.             ->where('p.receiptNumber LIKE :pattern')
  900.             ->setParameter('pattern''REC-' $year '%')
  901.             ->orderBy('p.receiptNumber''DESC')
  902.             ->setMaxResults(1)
  903.             ->getQuery()
  904.             ->getOneOrNullResult();
  905.         
  906.         if ($lastReceipt && $lastReceipt->getReceiptNumber()) {
  907.             // Extraire le numéro séquentiel (format: REC-YYYYNNN-HASH)
  908.             $parts explode('-'$lastReceipt->getReceiptNumber());
  909.             if (count($parts) >= 2) {
  910.                 $lastNumber = (int) substr($parts[1], 4); // Extraire NNN de YYYYNNN
  911.                 $newNumber $lastNumber 1;
  912.             } else {
  913.                 $newNumber 1;
  914.             }
  915.         } else {
  916.             $newNumber 1;
  917.         }
  918.         
  919.         // Créer la partie base du numéro
  920.         $baseNumber sprintf('REC-%s%03d'$year$newNumber);
  921.         
  922.         // Générer un hash sécurisé basé sur des données uniques
  923.         $secret 'PaySolution_Secret_Key_2026'// Clé secrète (à mettre dans .env en production)
  924.         $timestamp microtime(true);
  925.         $randomBytes bin2hex(random_bytes(4));
  926.         
  927.         // Créer un hash unique et imprévisible
  928.         $hashData $baseNumber $timestamp $randomBytes $secret;
  929.         $hash strtoupper(substr(hash('sha256'$hashData), 06));
  930.         
  931.         return sprintf('%s-%s'$baseNumber$hash);
  932.     }
  933.     /**
  934.      * @Route("/verify-receipt", name="verify_receipt_page", methods={"GET"})
  935.      */
  936.     public function verifyReceiptPage(): Response
  937.     {
  938.         return $this->render('public/verify_receipt.html.twig');
  939.     }
  940.     /**
  941.      * @Route("/api/verify-receipt", name="verify_receipt_api", methods={"POST"})
  942.      */
  943.     public function verifyReceiptApi(Request $request): JsonResponse
  944.     {
  945.         $data json_decode($request->getContent(), true);
  946.         $code $data['code'] ?? '';
  947.         if (empty($code)) {
  948.             return new JsonResponse([
  949.                 'success' => false,
  950.                 'message' => 'Code de vérification requis'
  951.             ], 400);
  952.         }
  953.         // Nettoyer le code
  954.         $code trim($code);
  955.         $paymentOrderRepo $this->entityManager->getRepository(PaymentOrder::class);
  956.         
  957.         // Rechercher par receiptNumber
  958.         $paymentOrder $paymentOrderRepo->findOneBy(['receiptNumber' => $code]);
  959.         if (!$paymentOrder) {
  960.             return new JsonResponse([
  961.                 'success' => false,
  962.                 'message' => 'Reçu introuvable. Ce code ne correspond à aucun paiement dans notre système.'
  963.             ], 404);
  964.         }
  965.         if (!$paymentOrder->getStatus()) {
  966.             return new JsonResponse([
  967.                 'success' => false,
  968.                 'message' => 'Ce bon de paiement n\'a pas encore été validé.'
  969.             ], 400);
  970.         }
  971.         // Récupérer les informations du client
  972.         $customer null;
  973.         $contracts = [];
  974.         foreach ($paymentOrder->getInvoicePayments() as $payment) {
  975.             if (!$customer) {
  976.                 $customer $payment->getInvoice()->getContract()->getCustomer();
  977.             }
  978.             $contractNum $payment->getInvoice()->getContract()->getContractNumber();
  979.             if (!in_array($contractNum$contracts)) {
  980.                 $contracts[] = $contractNum;
  981.             }
  982.         }
  983.         // Récupérer les factures
  984.         $invoices = [];
  985.         foreach ($paymentOrder->getInvoicePayments() as $payment) {
  986.             $invoice $payment->getInvoice();
  987.             $invoices[] = [
  988.                 'number' => $invoice->getInvoiceNumber(),
  989.                 'contract' => $invoice->getContract()->getContractNumber(),
  990.                 'period' => $invoice->getPeriod(),
  991.                 'amount' => $payment->getAmount()
  992.             ];
  993.         }
  994.         return new JsonResponse([
  995.             'success' => true,
  996.             'message' => 'Reçu authentique et validé',
  997.             'data' => [
  998.                 'receiptNumber' => $paymentOrder->getReceiptNumber(),
  999.                 'status' => 'VALIDÉ ET PAYÉ',
  1000.                 'validatedAt' => $paymentOrder->getValidatedAt() ? $paymentOrder->getValidatedAt()->format('d/m/Y à H:i') : null,
  1001.                 'totalAmount' => number_format($paymentOrder->getTotalAmount(), 2','' ') . ' FC',
  1002.                 'customer' => [
  1003.                     'name' => $customer $customer->getName() : 'N/A',
  1004.                     'contracts' => implode(', '$contracts)
  1005.                 ],
  1006.                 'payment' => [
  1007.                     'transactionNumber' => $paymentOrder->getTransactionNumber(),
  1008.                     'accountNumber' => $paymentOrder->getAccountNumber(),
  1009.                     'transactionDate' => $paymentOrder->getTransactionDate() ? $paymentOrder->getTransactionDate()->format('d/m/Y à H:i') : null
  1010.                 ],
  1011.                 'invoices' => $invoices
  1012.             ]
  1013.         ]);
  1014.     }
  1015.     /**
  1016.      * @Route("/client/logout", name="client_logout", methods={"GET"})
  1017.      */
  1018.     public function logout(SessionInterface $session): Response
  1019.     {
  1020.         $session->clear();
  1021.         return $this->redirectToRoute('client_login');
  1022.     }
  1023. }