<?php
namespace App\Controller;
use App\Entity\Contract;
use App\Entity\Invoice;
use App\Entity\InvoicePayment;
use App\Entity\PaymentOrder;
use App\Repository\ContractRepository;
use App\Repository\InvoiceRepository;
use App\Repository\InvoicePaymentRepository;
use App\Repository\PaymentOrderRepository;
use App\Repository\TaxeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
class ClientSpaceController extends AbstractController
{
private $entityManager;
private $contractRepository;
private $invoiceRepository;
private $paymentRepository;
private $taxeRepository;
public function __construct(
EntityManagerInterface $entityManager,
ContractRepository $contractRepository,
InvoiceRepository $invoiceRepository,
InvoicePaymentRepository $paymentRepository,
TaxeRepository $taxeRepository
) {
$this->entityManager = $entityManager;
$this->contractRepository = $contractRepository;
$this->invoiceRepository = $invoiceRepository;
$this->paymentRepository = $paymentRepository;
$this->taxeRepository = $taxeRepository;
}
/**
* @Route("/client/login", name="client_login", methods={"GET", "POST"})
*/
public function login(Request $request, SessionInterface $session): Response
{
// Si déjà connecté, rediriger vers le dashboard
if ($session->get('client_contract_id')) {
return $this->redirectToRoute('client_dashboard');
}
$error = null;
if ($request->isMethod('POST')) {
$contractNumber = $request->request->get('contract_number');
// Rechercher le contrat par son numéro
$contract = $this->contractRepository->findOneBy(['contractNumber' => $contractNumber]);
if ($contract && $contract->isActive()) {
// Stocker l'ID du contrat en session
$session->set('client_contract_id', $contract->getId());
$session->set('client_contract_number', $contract->getContractNumber());
$session->set('client_customer_name', $contract->getCustomer()->getName());
return $this->redirectToRoute('client_dashboard');
} else {
$error = 'Numéro de contrat invalide ou contrat inactif.';
}
}
return $this->render('client_space/login.html.twig', [
'error' => $error
]);
}
/**
* @Route("/client/dashboard", name="client_dashboard", methods={"GET"})
*/
public function dashboard(SessionInterface $session): Response
{
// Vérifier si le client est connecté
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
// Récupérer le contrat de connexion
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer TOUS les contrats du client
$allContracts = $this->contractRepository->findBy(['customer' => $customer]);
// Récupérer TOUTES les factures de TOUS les contrats du client
$allInvoices = $this->entityManager->createQueryBuilder()
->select('i')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->orderBy('i.issueDate', 'DESC')
->getQuery()
->getResult();
// Récupérer les taxes applicables pour le client
$applicableTaxes = $this->taxeRepository->findApplicableTaxesForCustomer($customer);
// Calculer les statistiques globales
$totalInvoices = count($allInvoices);
$totalAmount = 0;
$totalPaid = 0;
$totalUnpaid = 0;
$overdueCount = 0;
$now = new \DateTime();
foreach ($allInvoices as $invoice) {
$totalAmount += $invoice->getTotalAmount();
$paidAmount = $invoice->getPaidAmount();
$totalPaid += $paidAmount;
// Calculer le montant restant basé sur la part SNEL uniquement
$snelAmount = $invoice->getSnelAmount($applicableTaxes);
$remaining = $snelAmount - $paidAmount;
$totalUnpaid += max(0, $remaining);
// Vérifier si la facture est en retard
if ($remaining > 0 && $invoice->getDueDate() < $now) {
$overdueCount++;
}
}
// Récupérer les 5 dernières factures
$recentInvoices = array_slice($allInvoices, 0, 5);
// Récupérer les derniers paiements validés de tous les contrats
$recentPayments = $this->entityManager->createQueryBuilder()
->select('p', 'i')
->from('App\Entity\InvoicePayment', 'p')
->join('p.invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->andWhere('p.validated = :validated')
->andWhere('p.paymentDate IS NOT NULL')
->andWhere('p.deletedAt IS NULL')
->setParameter('customer', $customer)
->setParameter('validated', true)
->orderBy('p.paymentDate', 'DESC')
->setMaxResults(3)
->getQuery()
->getResult();
// Récupérer les derniers bons de perception du client
$recentPaymentOrders = $this->entityManager->createQueryBuilder()
->select('po')
->from('App\Entity\PaymentOrder', 'po')
->join('po.invoicePayments', 'ip')
->join('ip.invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->groupBy('po.id')
->orderBy('po.createdAt', 'DESC')
->setMaxResults(3)
->getQuery()
->getResult();
return $this->render('client_space/dashboard.html.twig', [
'contract' => $contract,
'customer' => $customer,
'allContracts' => $allContracts,
'totalContracts' => count($allContracts),
'totalInvoices' => $totalInvoices,
'totalAmount' => $totalAmount,
'totalPaid' => $totalPaid,
'totalUnpaid' => $totalUnpaid,
'overdueCount' => $overdueCount,
'recentInvoices' => $recentInvoices,
'recentPayments' => $recentPayments,
'recentPaymentOrders' => $recentPaymentOrders,
'applicableTaxes' => $applicableTaxes,
]);
}
/**
* @Route("/client/invoices", name="client_invoices", methods={"GET"})
*/
public function invoices(SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer TOUTES les factures de TOUS les contrats du client
$allInvoices = $this->entityManager->createQueryBuilder()
->select('i', 'c')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->orderBy('i.period', 'DESC')
->addOrderBy('i.issueDate', 'DESC')
->getQuery()
->getResult();
// Regrouper les factures par période
$invoicesByPeriod = [];
foreach ($allInvoices as $invoice) {
$period = $invoice->getPeriod();
if (!isset($invoicesByPeriod[$period])) {
$invoicesByPeriod[$period] = [
'invoices' => [],
'totalAmount' => 0,
'totalPaid' => 0,
'totalRemaining' => 0,
'allPaid' => true,
'hasOverdue' => false,
];
}
$paidAmount = $invoice->getPaidAmount();
$remaining = $invoice->getTotalAmount() - $paidAmount;
$invoicesByPeriod[$period]['invoices'][] = $invoice;
$invoicesByPeriod[$period]['totalAmount'] += $invoice->getTotalAmount();
$invoicesByPeriod[$period]['totalPaid'] += $paidAmount;
$invoicesByPeriod[$period]['totalRemaining'] += $remaining;
if ($remaining > 0) {
$invoicesByPeriod[$period]['allPaid'] = false;
if ($invoice->getDueDate() < new \DateTime()) {
$invoicesByPeriod[$period]['hasOverdue'] = true;
}
}
}
// Calculer les taxes applicables au client
$applicableTaxes = $this->taxeRepository->findApplicableTaxesForCustomer($customer);
// Récupérer les moyens de paiement actifs
$paymentGateways = $this->entityManager->getRepository('App\Entity\PaymentGateway')
->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
return $this->render('client_space/invoices.html.twig', [
'contract' => $contract,
'customer' => $customer,
'invoicesByPeriod' => $invoicesByPeriod,
'paymentGateways' => $paymentGateways,
'applicableTaxes' => $applicableTaxes,
]);
}
/**
* @Route("/client/invoices/{id}/taxes-detail", name="client_invoice_taxes_detail", methods={"GET"})
*/
public function invoiceTaxesDetail(int $id, SessionInterface $session): JsonResponse
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
return new JsonResponse(['success' => false, 'message' => 'Contrat introuvable'], 404);
}
$invoice = $this->invoiceRepository->find($id);
if (!$invoice) {
return new JsonResponse(['success' => false, 'message' => 'Facture introuvable'], 404);
}
// Vérifier que la facture appartient bien au client
if ($invoice->getContract()->getCustomer()->getId() !== $contract->getCustomer()->getId()) {
return new JsonResponse(['success' => false, 'message' => 'Accès non autorisé'], 403);
}
// Récupérer les taxes depuis le JSON taxesBreakdown
$taxesBreakdown = $invoice->getTaxesBreakdown();
$taxes = [];
$totalTaxAmount = 0;
if ($taxesBreakdown && isset($taxesBreakdown['taxes'])) {
foreach ($taxesBreakdown['taxes'] as $tax) {
$taxes[] = [
'name' => $tax['name'],
'percentage' => $tax['percentage'],
'amount' => $tax['amount']
];
$totalTaxAmount += $tax['amount'];
}
}
return new JsonResponse([
'success' => true,
'invoiceNumber' => $invoice->getInvoiceNumber(),
'taxes' => $taxes,
'totalTaxAmount' => $totalTaxAmount,
'ttcAmount' => $invoice->getTotalAmount()
]);
}
/**
* @Route("/client/payments-history", name="client_payments_history", methods={"GET"})
*/
public function paymentsHistory(SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer les paiements validés de TOUS les contrats du client
// Un paiement est considéré comme validé si:
// - validated = true (paiement approuvé)
// - paymentDate existe (paiement effectué)
// - deletedAt est null (non supprimé)
$payments = $this->entityManager->createQueryBuilder()
->select('p', 'i', 'c')
->from('App\Entity\InvoicePayment', 'p')
->join('p.invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->andWhere('p.validated = :validated')
->andWhere('p.paymentDate IS NOT NULL')
->andWhere('p.deletedAt IS NULL')
->setParameter('customer', $customer)
->setParameter('validated', true)
->orderBy('p.paymentDate', 'DESC')
->getQuery()
->getResult();
// Récupérer TOUTES les factures de TOUS les contrats du client
$allInvoices = $this->entityManager->createQueryBuilder()
->select('i', 'c')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->orderBy('i.issueDate', 'DESC')
->getQuery()
->getResult();
// Récupérer toutes les périodes uniques pour le filtre
$periods = $this->entityManager->createQueryBuilder()
->select('DISTINCT i.period')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->orderBy('i.period', 'DESC')
->getQuery()
->getResult();
// Récupérer tous les numéros de factures uniques pour le filtre
$invoiceNumbers = $this->entityManager->createQueryBuilder()
->select('DISTINCT i.invoiceNumber')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->orderBy('i.invoiceNumber', 'DESC')
->getQuery()
->getResult();
// Préparer les données pour le graphique (sérialisation simplifiée)
$paymentsForChart = [];
foreach ($payments as $payment) {
$paymentsForChart[] = [
'amount' => (float) $payment->getAmount(),
'paymentDate' => $payment->getPaymentDate() ? $payment->getPaymentDate()->format('Y-m-d') : null,
];
}
return $this->render('client_space/payments_history.html.twig', [
'contract' => $contract,
'customer' => $customer,
'payments' => $payments,
'paymentsForChart' => $paymentsForChart,
'allInvoices' => $allInvoices,
'periods' => array_column($periods, 'period'),
'invoiceNumbers' => array_column($invoiceNumbers, 'invoiceNumber'),
]);
}
/**
* @Route("/client/invoice/{id}", name="client_invoice_detail", methods={"GET"})
*/
public function invoiceDetail(int $id, SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer la facture et vérifier qu'elle appartient au client
$invoice = $this->entityManager->createQueryBuilder()
->select('i', 'c')
->from('App\Entity\Invoice', 'i')
->join('i.contract', 'c')
->where('i.id = :id')
->andWhere('c.customer = :customer')
->setParameter('id', $id)
->setParameter('customer', $customer)
->getQuery()
->getOneOrNullResult();
if (!$invoice) {
throw $this->createNotFoundException('Facture non trouvée');
}
// Récupérer les taxes applicables
$applicableTaxes = $this->taxeRepository->findApplicableTaxesForCustomer($customer);
// Récupérer les paiements de cette facture
$payments = $this->entityManager->createQueryBuilder()
->select('p')
->from('App\Entity\InvoicePayment', 'p')
->where('p.invoice = :invoice')
->andWhere('p.paymentDate IS NOT NULL')
->andWhere('p.deletedAt IS NULL')
->andWhere('(p.validated = :validated OR p.validated IS NULL)')
->setParameter('invoice', $invoice)
->setParameter('validated', true)
->orderBy('p.paymentDate', 'DESC')
->getQuery()
->getResult();
return $this->render('client_space/invoice_detail.html.twig', [
'contract' => $contract,
'customer' => $customer,
'invoice' => $invoice,
'applicableTaxes' => $applicableTaxes,
'payments' => $payments,
]);
}
/**
* @Route("/client/contract-info", name="client_contract_info", methods={"GET"})
*/
public function contractInfo(SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer tous les contrats du client
$allContracts = $this->contractRepository->findBy(
['customer' => $customer],
['createdAt' => 'DESC']
);
// Préparer les statistiques pour chaque contrat
$contractsData = [];
foreach ($allContracts as $c) {
$invoices = $c->getInvoices();
$totalInvoices = count($invoices);
$paidInvoices = 0;
$partiallyPaidInvoices = 0;
$unpaidInvoices = 0;
$totalAmount = 0;
$paidAmount = 0;
foreach ($invoices as $invoice) {
$totalAmount += $invoice->getTotalAmount();
$paidAmount += $invoice->getPaidAmount();
if ($invoice->isFullyPaid()) {
$paidInvoices++;
} elseif ($invoice->getPaidAmount() > 0) {
$partiallyPaidInvoices++;
} else {
$unpaidInvoices++;
}
}
$contractsData[] = [
'contract' => $c,
'totalInvoices' => $totalInvoices,
'paidInvoices' => $paidInvoices,
'partiallyPaidInvoices' => $partiallyPaidInvoices,
'unpaidInvoices' => $unpaidInvoices,
'totalAmount' => $totalAmount,
'paidAmount' => $paidAmount,
'remainingAmount' => $totalAmount - $paidAmount,
];
}
return $this->render('client_space/contract_info.html.twig', [
'contract' => $contract,
'customer' => $customer,
'contractsData' => $contractsData,
]);
}
/**
* @Route("/client/contract/{id}/invoices", name="client_contract_invoices", methods={"GET"})
*/
public function contractInvoices(int $id, SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$currentContract = $this->contractRepository->find($contractId);
if (!$currentContract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
// Récupérer le contrat demandé
$contract = $this->contractRepository->find($id);
if (!$contract) {
throw $this->createNotFoundException('Contrat introuvable');
}
// Vérifier que le contrat appartient bien au client
if ($contract->getCustomer()->getId() !== $currentContract->getCustomer()->getId()) {
throw $this->createAccessDeniedException('Accès non autorisé');
}
$customer = $contract->getCustomer();
// Récupérer toutes les factures de ce contrat
$invoices = $this->entityManager->createQueryBuilder()
->select('i')
->from('App\Entity\Invoice', 'i')
->where('i.contract = :contract')
->setParameter('contract', $contract)
->orderBy('i.period', 'DESC')
->addOrderBy('i.issueDate', 'DESC')
->getQuery()
->getResult();
// Regrouper les factures par période
$invoicesByPeriod = [];
foreach ($invoices as $invoice) {
$period = $invoice->getPeriod();
if (!isset($invoicesByPeriod[$period])) {
$invoicesByPeriod[$period] = [
'invoices' => [],
'totalAmount' => 0,
'totalPaid' => 0,
'totalRemaining' => 0,
'allPaid' => true,
'hasOverdue' => false,
];
}
$paidAmount = $invoice->getPaidAmount();
$remaining = $invoice->getTotalAmount() - $paidAmount;
$invoicesByPeriod[$period]['invoices'][] = $invoice;
$invoicesByPeriod[$period]['totalAmount'] += $invoice->getTotalAmount();
$invoicesByPeriod[$period]['totalPaid'] += $paidAmount;
$invoicesByPeriod[$period]['totalRemaining'] += $remaining;
if ($remaining > 0) {
$invoicesByPeriod[$period]['allPaid'] = false;
if ($invoice->getDueDate() < new \DateTime()) {
$invoicesByPeriod[$period]['hasOverdue'] = true;
}
}
}
// Calculer les taxes applicables au client
$applicableTaxes = $this->taxeRepository->findApplicableTaxesForCustomer($customer);
// Récupérer les moyens de paiement actifs
$paymentGateways = $this->entityManager->getRepository('App\Entity\PaymentGateway')
->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
return $this->render('client_space/contract_invoices.html.twig', [
'contract' => $contract,
'customer' => $customer,
'invoicesByPeriod' => $invoicesByPeriod,
'paymentGateways' => $paymentGateways,
'applicableTaxes' => $applicableTaxes,
]);
}
/**
* @Route("/client/payment-orders", name="client_payment_orders", methods={"GET"})
*/
public function paymentOrders(SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer tous les bons de perception du client
$paymentOrders = $this->entityManager->createQueryBuilder()
->select('po')
->from('App\Entity\PaymentOrder', 'po')
->join('po.invoicePayments', 'ip')
->join('ip.invoice', 'i')
->join('i.contract', 'c')
->where('c.customer = :customer')
->setParameter('customer', $customer)
->groupBy('po.id')
->orderBy('po.createdAt', 'DESC')
->getQuery()
->getResult();
return $this->render('client_space/payment_orders.html.twig', [
'contract' => $contract,
'customer' => $customer,
'paymentOrders' => $paymentOrders,
]);
}
/**
* @Route("/client/payment-order/{id}", name="client_payment_order_detail", methods={"GET"})
*/
public function paymentOrderDetail(int $id, SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
$session->clear();
return $this->redirectToRoute('client_login');
}
$customer = $contract->getCustomer();
// Récupérer le bon de perception avec vérification d'accès
$paymentOrder = $this->entityManager->createQueryBuilder()
->select('po', 'ip', 'i', 'c')
->from('App\Entity\PaymentOrder', 'po')
->join('po.invoicePayments', 'ip')
->join('ip.invoice', 'i')
->join('i.contract', 'c')
->where('po.id = :id')
->andWhere('c.customer = :customer')
->setParameter('id', $id)
->setParameter('customer', $customer)
->getQuery()
->getOneOrNullResult();
if (!$paymentOrder) {
throw $this->createNotFoundException('Bon de perception non trouvé');
}
// Récupérer les moyens de paiement en ligne actifs
$paymentGateways = $this->entityManager->getRepository('App\Entity\PaymentGateway')
->findBy(['isActive' => true], ['displayOrder' => 'ASC']);
// Récupérer les comptes actifs et disponibles pour paiement
$accounts = $this->entityManager->getRepository('App\Entity\Account')
->findBy([
'isActive' => true,
'isAvailableForPayment' => true
], ['accountNumber' => 'ASC']);
// Calculer le montant SNEL total et les taxes totales
$totalSnelAmount = 0;
$totalTaxAmount = 0;
$applicableTaxes = $this->taxeRepository->findApplicableTaxesForCustomer($customer);
foreach ($paymentOrder->getInvoicePayments() as $invoicePayment) {
$invoice = $invoicePayment->getInvoice();
$totalSnelAmount += $invoice->getSnelAmount($applicableTaxes);
$totalTaxAmount += $invoice->getTaxAmount($applicableTaxes);
}
return $this->render('client_space/payment_order_detail.html.twig', [
'contract' => $contract,
'customer' => $customer,
'paymentGateways' => $paymentGateways,
'paymentOrder' => $paymentOrder,
'accounts' => $accounts,
'totalSnelAmount' => $totalSnelAmount,
'totalTaxAmount' => $totalTaxAmount,
]);
}
/**
* @Route("/client/payment-order/{id}/upload-proof", name="client_upload_proof_of_payment", methods={"POST"})
*/
public function uploadProofOfPayment(int $id, Request $request, SessionInterface $session): JsonResponse
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
return new JsonResponse(['success' => false, 'message' => 'Contrat introuvable'], 404);
}
$customer = $contract->getCustomer();
// Récupérer le bon de perception avec vérification d'accès
$paymentOrder = $this->entityManager->createQueryBuilder()
->select('po', 'ip', 'i', 'c')
->from('App\Entity\PaymentOrder', 'po')
->join('po.invoicePayments', 'ip')
->join('ip.invoice', 'i')
->join('i.contract', 'c')
->where('po.id = :id')
->andWhere('c.customer = :customer')
->setParameter('id', $id)
->setParameter('customer', $customer)
->getQuery()
->getOneOrNullResult();
if (!$paymentOrder) {
return new JsonResponse(['success' => false, 'message' => 'Bon de perception non trouvé'], 404);
}
// Vérifier si le bon n'est pas déjà validé
if ($paymentOrder->getStatus()) {
return new JsonResponse(['success' => false, 'message' => 'Ce bon de perception est déjà validé'], 400);
}
// Récupérer le fichier uploadé
$file = $request->files->get('proofFile');
if (!$file) {
return new JsonResponse(['success' => false, 'message' => 'Aucun fichier fourni'], 400);
}
// Valider le fichier
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf'];
if (!in_array($file->getMimeType(), $allowedMimeTypes)) {
return new JsonResponse(['success' => false, 'message' => 'Type de fichier non autorisé. Formats acceptés: JPG, PNG, PDF'], 400);
}
if ($file->getSize() > 5 * 1024 * 1024) { // 5MB
return new JsonResponse(['success' => false, 'message' => 'Le fichier est trop volumineux. Taille maximale: 5MB'], 400);
}
try {
// Créer le répertoire s'il n'existe pas
$uploadDir = $this->getParameter('kernel.project_dir') . '/public/uploads/proof_of_payment';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// Générer un nom de fichier unique
$fileName = uniqid() . '_' . $paymentOrder->getNumAuto() . '.' . $file->guessExtension();
$file->move($uploadDir, $fileName);
// Mettre à jour le bon de paiement avec toutes les informations
$paymentOrder->setProofOfPayment($fileName);
// Informations de transaction
$transactionNumber = $request->request->get('transactionNumber');
if ($transactionNumber) {
$paymentOrder->setTransactionNumber($transactionNumber);
}
$transactionDate = $request->request->get('transactionDate');
if ($transactionDate) {
$paymentOrder->setTransactionDate(new \DateTime($transactionDate));
}
$paymentMethod = $request->request->get('paymentMethod');
if ($paymentMethod) {
$paymentOrder->setPaymentMethod($paymentMethod);
}
$accountNumber = $request->request->get('accountNumber');
if ($accountNumber) {
$paymentOrder->setAccountNumber($accountNumber);
}
$agentCode = $request->request->get('agentCode');
if ($agentCode) {
$paymentOrder->setAgentCode($agentCode);
}
$agencyCode = $request->request->get('agencyCode');
if ($agencyCode) {
$paymentOrder->setAgencyCode($agencyCode);
}
// Ajouter les notes si fournies
$notes = $request->request->get('notes');
if ($notes) {
$existingNotes = $paymentOrder->getNotes();
$newNotes = $existingNotes ? $existingNotes . "\n\n--- Preuve de paiement ---\n" . $notes : $notes;
$paymentOrder->setNotes($newNotes);
}
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'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.'
]);
} catch (\Exception $e) {
return new JsonResponse([
'success' => false,
'message' => 'Erreur lors de l\'upload: ' . $e->getMessage()
], 500);
}
}
/**
* @Route("/client/generate-payment-order", name="client_generate_payment_order", methods={"POST"})
*/
public function generatePaymentOrder(Request $request, SessionInterface $session): JsonResponse
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return new JsonResponse(['error' => 'Non authentifié'], 401);
}
$contract = $this->contractRepository->find($contractId);
if (!$contract) {
return new JsonResponse(['error' => 'Contrat introuvable'], 404);
}
$data = json_decode($request->getContent(), true);
$cartItems = $data['cart'] ?? [];
$paymentMethod = $data['paymentMethod'] ?? 'Non spécifié';
if (empty($cartItems)) {
return new JsonResponse(['error' => 'Panier vide'], 400);
}
try {
// Créer le bon de perception
$paymentOrder = new PaymentOrder();
$paymentOrder->setPaymentMethod($paymentMethod);
$totalAmount = 0;
foreach ($cartItems as $item) {
$invoice = $this->entityManager->getRepository('App\Entity\Invoice')->find($item['invoiceId']);
if (!$invoice) {
continue;
}
$invoicePayment = new InvoicePayment();
$invoicePayment->setInvoice($invoice);
$invoicePayment->setAmount($item['amount']);
//$invoicePayment->setPaymentDate(new \DateTime());
//$invoicePayment->setPaymentMethod('bank_order');
// createdBy est null pour les paiements créés depuis l'espace client
$paymentOrder->addInvoicePayment($invoicePayment);
$this->entityManager->persist($invoicePayment);
$totalAmount += (float) $item['amount'];
}
$paymentOrder->setTotalAmount($totalAmount);
$this->entityManager->persist($paymentOrder);
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'numAuto' => $paymentOrder->getNumAuto(),
'paymentOrderId' => $paymentOrder->getId(),
'totalAmount' => $totalAmount,
'message' => 'Bon de perception généré avec succès'
]);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'Erreur lors de la génération: ' . $e->getMessage()], 500);
}
}
/**
* @Route("/client/download-payment-order/{id}", name="client_download_payment_order", methods={"GET"})
*/
public function downloadPaymentOrder(int $id, SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$paymentOrderRepo = $this->entityManager->getRepository(PaymentOrder::class);
$paymentOrder = $paymentOrderRepo->find($id);
if (!$paymentOrder) {
throw $this->createNotFoundException('Bon de perception introuvable');
}
// Vérifier que le client a accès à ce bon (via les factures)
$contract = $this->contractRepository->find($contractId);
$customer = $contract->getCustomer();
// Vérifier que toutes les factures appartiennent bien au client
$hasAccess = false;
foreach ($paymentOrder->getInvoicePayments() as $payment) {
if ($payment->getInvoice()->getContract()->getCustomer()->getId() === $customer->getId()) {
$hasAccess = true;
break;
}
}
if (!$hasAccess) {
throw $this->createNotFoundException('Bon de perception introuvable');
}
return $this->render('client_space/payment_order_pdf.html.twig', [
'paymentOrder' => $paymentOrder,
'customer' => $customer,
]);
}
/**
* @Route("/client/download-payment-receipt/{id}", name="client_download_payment_receipt", methods={"GET"})
*/
public function downloadPaymentReceipt(int $id, SessionInterface $session): Response
{
$contractId = $session->get('client_contract_id');
if (!$contractId) {
return $this->redirectToRoute('client_login');
}
$paymentOrderRepo = $this->entityManager->getRepository(PaymentOrder::class);
$paymentOrder = $paymentOrderRepo->find($id);
if (!$paymentOrder) {
throw $this->createNotFoundException('Bon de paiement introuvable');
}
// Vérifier que le bon est validé
if (!$paymentOrder->getStatus()) {
throw $this->createNotFoundException('Le reçu n\'est disponible que pour les bons validés');
}
// Vérifier que le client a accès à ce bon (via les factures)
$contract = $this->contractRepository->find($contractId);
$customer = $contract->getCustomer();
// Vérifier que toutes les factures appartiennent bien au client
$hasAccess = false;
foreach ($paymentOrder->getInvoicePayments() as $payment) {
if ($payment->getInvoice()->getContract()->getCustomer()->getId() === $customer->getId()) {
$hasAccess = true;
break;
}
}
if (!$hasAccess) {
throw $this->createNotFoundException('Bon de paiement introuvable');
}
// Générer le numéro de reçu s'il n'existe pas
if (!$paymentOrder->getReceiptNumber()) {
$receiptNumber = $this->generateReceiptNumber();
$paymentOrder->setReceiptNumber($receiptNumber);
$this->entityManager->flush();
}
return $this->render('client_space/payment_receipt_pdf.html.twig', [
'paymentOrder' => $paymentOrder,
'customer' => $customer,
]);
}
/**
* Génère un numéro de reçu unique au format REC-YYYYNNN-HASH
*/
private function generateReceiptNumber(): string
{
$year = date('Y');
$paymentOrderRepo = $this->entityManager->getRepository(PaymentOrder::class);
// Trouver le dernier numéro de reçu de l'année
$lastReceipt = $paymentOrderRepo->createQueryBuilder('p')
->where('p.receiptNumber LIKE :pattern')
->setParameter('pattern', 'REC-' . $year . '%')
->orderBy('p.receiptNumber', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
if ($lastReceipt && $lastReceipt->getReceiptNumber()) {
// Extraire le numéro séquentiel (format: REC-YYYYNNN-HASH)
$parts = explode('-', $lastReceipt->getReceiptNumber());
if (count($parts) >= 2) {
$lastNumber = (int) substr($parts[1], 4); // Extraire NNN de YYYYNNN
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
} else {
$newNumber = 1;
}
// Créer la partie base du numéro
$baseNumber = sprintf('REC-%s%03d', $year, $newNumber);
// Générer un hash sécurisé basé sur des données uniques
$secret = 'PaySolution_Secret_Key_2026'; // Clé secrète (à mettre dans .env en production)
$timestamp = microtime(true);
$randomBytes = bin2hex(random_bytes(4));
// Créer un hash unique et imprévisible
$hashData = $baseNumber . $timestamp . $randomBytes . $secret;
$hash = strtoupper(substr(hash('sha256', $hashData), 0, 6));
return sprintf('%s-%s', $baseNumber, $hash);
}
/**
* @Route("/verify-receipt", name="verify_receipt_page", methods={"GET"})
*/
public function verifyReceiptPage(): Response
{
return $this->render('public/verify_receipt.html.twig');
}
/**
* @Route("/api/verify-receipt", name="verify_receipt_api", methods={"POST"})
*/
public function verifyReceiptApi(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$code = $data['code'] ?? '';
if (empty($code)) {
return new JsonResponse([
'success' => false,
'message' => 'Code de vérification requis'
], 400);
}
// Nettoyer le code
$code = trim($code);
$paymentOrderRepo = $this->entityManager->getRepository(PaymentOrder::class);
// Rechercher par receiptNumber
$paymentOrder = $paymentOrderRepo->findOneBy(['receiptNumber' => $code]);
if (!$paymentOrder) {
return new JsonResponse([
'success' => false,
'message' => 'Reçu introuvable. Ce code ne correspond à aucun paiement dans notre système.'
], 404);
}
if (!$paymentOrder->getStatus()) {
return new JsonResponse([
'success' => false,
'message' => 'Ce bon de paiement n\'a pas encore été validé.'
], 400);
}
// Récupérer les informations du client
$customer = null;
$contracts = [];
foreach ($paymentOrder->getInvoicePayments() as $payment) {
if (!$customer) {
$customer = $payment->getInvoice()->getContract()->getCustomer();
}
$contractNum = $payment->getInvoice()->getContract()->getContractNumber();
if (!in_array($contractNum, $contracts)) {
$contracts[] = $contractNum;
}
}
// Récupérer les factures
$invoices = [];
foreach ($paymentOrder->getInvoicePayments() as $payment) {
$invoice = $payment->getInvoice();
$invoices[] = [
'number' => $invoice->getInvoiceNumber(),
'contract' => $invoice->getContract()->getContractNumber(),
'period' => $invoice->getPeriod(),
'amount' => $payment->getAmount()
];
}
return new JsonResponse([
'success' => true,
'message' => 'Reçu authentique et validé',
'data' => [
'receiptNumber' => $paymentOrder->getReceiptNumber(),
'status' => 'VALIDÉ ET PAYÉ',
'validatedAt' => $paymentOrder->getValidatedAt() ? $paymentOrder->getValidatedAt()->format('d/m/Y à H:i') : null,
'totalAmount' => number_format($paymentOrder->getTotalAmount(), 2, ',', ' ') . ' FC',
'customer' => [
'name' => $customer ? $customer->getName() : 'N/A',
'contracts' => implode(', ', $contracts)
],
'payment' => [
'transactionNumber' => $paymentOrder->getTransactionNumber(),
'accountNumber' => $paymentOrder->getAccountNumber(),
'transactionDate' => $paymentOrder->getTransactionDate() ? $paymentOrder->getTransactionDate()->format('d/m/Y à H:i') : null
],
'invoices' => $invoices
]
]);
}
/**
* @Route("/client/logout", name="client_logout", methods={"GET"})
*/
public function logout(SessionInterface $session): Response
{
$session->clear();
return $this->redirectToRoute('client_login');
}
}