diff --git a/config/packages/vich_uploader.yaml b/config/packages/vich_uploader.yaml index 06ceeb2..add6bb8 100644 --- a/config/packages/vich_uploader.yaml +++ b/config/packages/vich_uploader.yaml @@ -45,3 +45,11 @@ vich_uploader: uri_prefix: /pdf/devis_audit upload_destination: '%kernel.project_dir%/public/pdf/devis_audit' namer: Vich\UploaderBundle\Naming\SmartUniqueNamer + payment_confirmed: + uri_prefix: /pdf/payment_confirmed + upload_destination: '%kernel.project_dir%/public/pdf/payment_confirmed' + namer: Vich\UploaderBundle\Naming\SmartUniqueNamer + payment_confirmed_signed: + uri_prefix: /pdf/payment_confirmed_signed + upload_destination: '%kernel.project_dir%/public/pdf/payment_confirmed_signed' + namer: Vich\UploaderBundle\Naming\SmartUniqueNamer diff --git a/migrations/Version20260123085450.php b/migrations/Version20260123085450.php new file mode 100644 index 0000000..b99f022 --- /dev/null +++ b/migrations/Version20260123085450.php @@ -0,0 +1,41 @@ +addSql('ALTER TABLE contrats_payments ADD payment_file_name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE contrats_payments ADD payment_file_size INT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrats_payments ADD payment_signed_file_name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE contrats_payments ADD payment_signed_file_size INT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrats_payments ADD update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN contrats_payments.update_at IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE contrats_payments DROP payment_file_name'); + $this->addSql('ALTER TABLE contrats_payments DROP payment_file_size'); + $this->addSql('ALTER TABLE contrats_payments DROP payment_signed_file_name'); + $this->addSql('ALTER TABLE contrats_payments DROP payment_signed_file_size'); + $this->addSql('ALTER TABLE contrats_payments DROP update_at'); + } +} diff --git a/src/Controller/ContratController.php b/src/Controller/ContratController.php index 2af9115..1515944 100644 --- a/src/Controller/ContratController.php +++ b/src/Controller/ContratController.php @@ -15,6 +15,7 @@ use App\Repository\ContratsRepository; use App\Repository\CustomerAddressRepository; use App\Repository\CustomerRepository; use App\Service\Mailer\Mailer; +use App\Service\Pdf\PlPdf; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; use App\Service\Signature\Client; @@ -24,14 +25,18 @@ use Symfony\Bundle\SecurityBundle\Security; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Vich\UploaderBundle\Templating\Helper\UploaderHelper; class ContratController extends AbstractController @@ -126,7 +131,7 @@ class ContratController extends AbstractController } #[Route('/reservation/gestion-contrat/{num}', name: 'gestion_contrat_view')] - public function gestionContratView(string $num,\App\Service\Stripe\Client $stripeClient,Client $client,Mailer $mailer,EntityManagerInterface $entityManager, Request $request, ContratsRepository $contratsRepository): Response + public function gestionContratView(string $num,UploaderHelper $uploaderHelper,KernelInterface $kernel,\App\Service\Stripe\Client $stripeClient,Client $client,Mailer $mailer,EntityManagerInterface $entityManager, Request $request, ContratsRepository $contratsRepository): Response { $contrat = $contratsRepository->findOneBy(['numReservation' => $num]); @@ -296,6 +301,37 @@ class ContratController extends AbstractController return new RedirectResponse($result['url']); } + if($request->query->get('idPaymentPdf')) { + $pl = $entityManager->getRepository(ContratsPayments::class)->find($request->query->get('idPaymentPdf')); + if($pl instanceof ContratsPayments && $pl->getState() == "complete") { + if ($pl->getPaymentSignedFileName() == null) { + $pdf = new PlPdf($kernel, $pl, $contrat); + $pdf->generate(); + $content = $pdf->Output('S'); + $tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf'; + file_put_contents($tmpSigned, $content); + + // On utilise UploadedFile pour simuler un upload propre pour VichUploader + $pl->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $pl->getId() . ".pdf", "application/pdf", null, true)); + $pl->setUpdateAt(new \DateTimeImmutable('now')); + $entityManager->persist($pl); + $entityManager->flush(); + $data = $client->autoSignConfirmedPayment($pl); + // 1. Gestion du PDF SIGNÉ + $tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf'; + $signedContent = file_get_contents($data); + file_put_contents($tmpSigned, $signedContent); + + // On utilise UploadedFile pour simuler un upload propre pour VichUploader + $pl->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $pl->getId() . ".pdf", "application/pdf", null, true)); + $pl->setUpdateAt(new \DateTimeImmutable('now')); + $entityManager->persist($pl); + $entityManager->flush(); + } + $path= $kernel->getProjectDir() . "/public".$uploaderHelper->asset($pl,'paymentSignedFile'); + return $this->file($path, 'confirmation-paiement.pdf', ResponseHeaderBag::DISPOSITION_INLINE); + } + } return $this->render('reservation/contrat/view.twig', [ 'contrat' => $contrat, diff --git a/src/Controller/Webhooks.php b/src/Controller/Webhooks.php index a94db33..2f89e72 100644 --- a/src/Controller/Webhooks.php +++ b/src/Controller/Webhooks.php @@ -6,19 +6,23 @@ use App\Entity\ContratsPayments; use App\Entity\Devis; use App\Entity\Product; use App\Entity\ProductReserve; +use App\Kernel; use App\Repository\ProductRepository; use App\Service\Mailer\Mailer; +use App\Service\Pdf\PlPdf; use App\Service\Stripe\Client; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Annotation\Route; class Webhooks extends AbstractController { #[Route(path: '/webhooks/payment-intent', name: 'webhooks_payment', options: ['sitemap' => false], methods: ['POST'])] - public function payment(Mailer $mailer, ProductRepository $productRepository, Request $request, Client $client, EntityManagerInterface $entityManager): Response + public function payment(Mailer $mailer,KernelInterface $kernel,\App\Service\Signature\Client $sig, ProductRepository $productRepository, Request $request, Client $client, EntityManagerInterface $entityManager): Response { // 1. Vérification de la signature if ($client->checkWebhooks($request, 'payment')) { @@ -34,7 +38,6 @@ class Webhooks extends AbstractController if ($paymentId) { $pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy(['paymentId' => $paymentId]); - if ($pl instanceof ContratsPayments && $pl->getState() !== "complete") { // MAJ du paiement @@ -65,6 +68,27 @@ class Webhooks extends AbstractController } } + $pdf = new PlPdf($kernel, $pl, $contrat); + $pdf->generate(); + $content = $pdf->Output('S'); + $tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf'; + file_put_contents($tmpSigned, $content); + + // On utilise UploadedFile pour simuler un upload propre pour VichUploader + $pl->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $pl->getId() . ".pdf", "application/pdf", null, true)); + $pl->setUpdateAt(new \DateTimeImmutable('now')); + $entityManager->persist($pl); + $entityManager->flush(); + $data = $sig->autoSignConfirmedPayment($pl); + // 1. Gestion du PDF SIGNÉ + $tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf'; + $signedContent = file_get_contents($data); + file_put_contents($tmpSigned, $signedContent); + + // On utilise UploadedFile pour simuler un upload propre pour VichUploader + $pl->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $pl->getId() . ".pdf", "application/pdf", null, true)); + $pl->setUpdateAt(new \DateTimeImmutable('now')); + $entityManager->persist($pl); $entityManager->flush(); // --- ENVOI DES EMAILS --- diff --git a/src/Entity/ContratsPayments.php b/src/Entity/ContratsPayments.php index 641ef27..28f5b30 100644 --- a/src/Entity/ContratsPayments.php +++ b/src/Entity/ContratsPayments.php @@ -5,8 +5,12 @@ namespace App\Entity; use App\Repository\ContratsPaymentsRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\HttpFoundation\File\File; +use Vich\UploaderBundle\Mapping\Attribute\Uploadable; +use Vich\UploaderBundle\Mapping\Attribute\UploadableField; #[ORM\Entity(repositoryClass: ContratsPaymentsRepository::class)] +#[Uploadable] class ContratsPayments { #[ORM\Id] @@ -38,6 +42,24 @@ class ContratsPayments #[ORM\Column(type: Types::ARRAY,nullable: true)] private array $card = []; + + #[UploadableField(mapping: 'payment_confirmed', fileNameProperty: 'paymentFileName', size: 'paymentFileSize')] + private ?File $paymentFile = null; + #[ORM\Column(nullable: true)] + private ?string $paymentFileName = null; + #[ORM\Column(nullable: true)] + private ?int $paymentFileSize = null; + + #[UploadableField(mapping: 'payment_confirmed_signed', fileNameProperty: 'paymentSignedFileName', size: 'paymentSignedFileSize')] + private ?File $paymentSignedFile = null; + #[ORM\Column(nullable: true)] + private ?string $paymentSignedFileName = null; + #[ORM\Column(nullable: true)] + private ?int $paymentSignedFileSize = null; + + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $updateAt = null; + public function getId(): ?int { return $this->id; @@ -138,4 +160,117 @@ class ContratsPayments return $this; } + + /** + * @return \DateTimeImmutable|null + */ + public function getUpdateAt(): ?\DateTimeImmutable + { + return $this->updateAt; + } + + /** + * @return File|null + */ + public function getPaymentFile(): ?File + { + return $this->paymentFile; + } + + + /** + * @return string|null + */ + public function getPaymentFileName(): ?string + { + return $this->paymentFileName; + } + + /** + * @return int|null + */ + public function getPaymentFileSize(): ?int + { + return $this->paymentFileSize; + } + + /** + * @return File|null + */ + public function getPaymentSignedFile(): ?File + { + return $this->paymentSignedFile; + } + + /** + * @return string|null + */ + public function getPaymentSignedFileName(): ?string + { + return $this->paymentSignedFileName; + } + + /** + * @return int|null + */ + public function getPaymentSignedFileSize(): ?int + { + return $this->paymentSignedFileSize; + } + + /** + * @param \DateTimeImmutable|null $updateAt + */ + public function setUpdateAt(?\DateTimeImmutable $updateAt): void + { + $this->updateAt = $updateAt; + } + + /** + * @param File|null $paymentFile + */ + public function setPaymentFile(?File $paymentFile): void + { + $this->paymentFile = $paymentFile; + } + + /** + * @param string|null $paymentFileName + */ + public function setPaymentFileName(?string $paymentFileName): void + { + $this->paymentFileName = $paymentFileName; + } + + /** + * @param int|null $paymentFileSize + */ + public function setPaymentFileSize(?int $paymentFileSize): void + { + $this->paymentFileSize = $paymentFileSize; + } + + /** + * @param File|null $paymentSignedFile + */ + public function setPaymentSignedFile(?File $paymentSignedFile): void + { + $this->paymentSignedFile = $paymentSignedFile; + } + + /** + * @param string|null $paymentSignedFileName + */ + public function setPaymentSignedFileName(?string $paymentSignedFileName): void + { + $this->paymentSignedFileName = $paymentSignedFileName; + } + + /** + * @param int|null $paymentSignedFileSize + */ + public function setPaymentSignedFileSize(?int $paymentSignedFileSize): void + { + $this->paymentSignedFileSize = $paymentSignedFileSize; + } } diff --git a/src/Service/Pdf/PlPdf.php b/src/Service/Pdf/PlPdf.php new file mode 100644 index 0000000..89e53f6 --- /dev/null +++ b/src/Service/Pdf/PlPdf.php @@ -0,0 +1,195 @@ +contratsPayments = $contratsPayments; + $this->contrat = $contrats; + $this->logo = $kernel->getProjectDir()."/public/provider/images/favicon.png"; + + $this->AliasNbPages(); + $this->SetAutoPageBreak(true, 35); + } + + /** + * Convertit l'UTF-8 en Windows-1252 pour FPDF et gère l'Euro + */ + private function clean(?string $text): string + { + if (!$text) return ''; + $text = iconv('UTF-8', 'windows-1252//TRANSLIT//IGNORE', $text); + return str_replace('€', chr(128), $text); + } + + /** + * Helper pour afficher le symbole Euro proprement + */ + private function euro(): string + { + return ' ' . chr(128); + } + + public function Header() + { + if ($this->page > 0 && !$this->isExtraPage) { + // --- LOGO ET INFOS SOCIÉTÉ --- + $this->SetY(10); + if (file_exists($this->logo)) { + $this->Image($this->logo, 10, 10, 15); // Un peu plus grand pour le reçu + $this->SetX(28); + } + + $this->SetFont('Arial', 'B', 14); + $this->SetTextColor(37, 99, 235); // Bleu Ludikevent + $this->Cell(100, 7, $this->clean('Lilian SEGARD - Ludik Event'), 0, 0, 'L'); + + // BADGE "REÇU" À DROITE + $this->SetFont('Arial', 'B', 11); + $this->SetFillColor(37, 99, 235); + $this->SetTextColor(255, 255, 255); + $this->SetX(150); + $this->Cell(50, 8, $this->clean('REÇU DE PAIEMENT'), 0, 1, 'C', true); + + // COORDONNÉES SOUS LE NOM + $this->SetY(17); + $this->SetX(28); + $this->SetFont('Arial', '', 8); + $this->SetTextColor(80, 80, 80); + $this->Cell(0, 4, $this->clean('SIRET : 930 488 408 00012 | 6 Rue du Château, 02800 Danizy'), 0, 1, 'L'); + $this->SetX(28); + $this->Cell(0, 4, $this->clean('Tél. : 06 14 17 24 47 | contact@ludikevent.fr'), 0, 1, 'L'); + + $this->Ln(12); + + // --- TITRE DU DOCUMENT ET RÉFÉRENCE --- + $this->SetFont('Arial', 'B', 15); + $this->SetTextColor(17, 24, 39); // Gris très foncé/Noir + $this->Cell(0, 10, $this->clean('Confirmation de règlement'), 0, 1, 'L'); + + $this->SetFont('Arial', 'B', 10); + $this->SetTextColor(107, 114, 128); // Gris intermédiaire + $this->Cell(0, 5, $this->clean('Référence Réservation : #' . $this->contrat->getNumReservation()), 0, 1, 'L'); + + // LIGNE DE SÉPARATION DESIGN + $this->Ln(2); + $this->SetDrawColor(37, 99, 235); + $this->SetLineWidth(0.8); + $this->Line(10, $this->GetY(), 40, $this->GetY()); // Petite ligne bleue + $this->SetDrawColor(229, 231, 235); // Gris clair + $this->SetLineWidth(0.2); + $this->Line(40, $this->GetY(), 200, $this->GetY()); // Ligne grise qui continue + + $this->Ln(5); + } + } + public function generate(): string + { + $this->AddPage(); + $this->renderContent(); + return $this->Output('S'); + } + + private function renderContent(): void + { + // --- 1. BLOC INFO PAIEMENT (Le "Reçu") --- + $this->SetY(55); + $this->SetFont('Arial', 'B', 11); + $this->SetFillColor(245, 245, 255); // Bleu très léger + $this->SetTextColor(37, 99, 235); + $this->Cell(0, 10, $this->clean(" DÉTAILS DU RÈGLEMENT"), 0, 1, 'L', true); + + $this->Ln(4); + $this->SetFont('Arial', '', 10); + $this->SetTextColor(50, 50, 50); + + // Tableau à deux colonnes pour les infos + $startY = $this->GetY(); + + $this->SetFont('Arial', 'B', 10); + $this->Cell(50, 8, $this->clean("Montant réglé :"), 0, 0); + $this->SetFont('Arial', 'B', 12); $this->SetTextColor(22, 163, 74); // Vert succès + $this->Cell(0, 8, number_format($this->contratsPayments->getAmount(), 2) . $this->euro(), 0, 1); + + $this->SetTextColor(50, 50, 50); + $this->SetFont('Arial', 'B', 10); + $this->Cell(50, 8, $this->clean("Date du paiement :"), 0, 0); + $this->SetFont('Arial', '', 10); + $this->Cell(0, 8, $this->contratsPayments->getValidateAt()->format('d/m/Y H:i'), 0, 1); + + $this->SetFont('Arial', 'B', 10); + $this->Cell(50, 8, $this->clean("Mode de règlement :"), 0, 0); + $this->SetFont('Arial', '', 10); + $this->Cell(0, 8, $this->clean($this->contratsPayments->getCard()['type'] ?? 'Carte Bancaire'), 0, 1); + + $this->SetFont('Arial', 'B', 10); + $this->Cell(50, 8, $this->clean("Référence transaction :"), 0, 0); + $this->SetFont('Arial', 'I', 9); + $this->Cell(0, 8, $this->clean($this->contratsPayments->getPaymentId() ?? 'N/A'), 0, 1); + + $this->Ln(10); + + // --- 2. BLOC CLIENT --- + $this->SetFont('Arial', 'B', 11); + $this->SetFillColor(250, 250, 250); + $this->SetTextColor(0, 0, 0); + $this->Cell(0, 10, $this->clean(" INFORMATIONS CLIENT"), 0, 1, 'L', true); + + $this->Ln(2); + $this->SetFont('Arial', '', 10); + $customer = $this->contrat->getCustomer(); + $this->MultiCell(0, 6, $this->clean( + $customer->getSurname() . " " . $customer->getName() . "\n" . + $customer->getEmail() . "\n" . + "Réservation N° " . $this->contrat->getNumReservation() + ), 0, 'L'); + + $this->Ln(15); + + // --- 3. MESSAGE DE CONFIRMATION --- + $this->SetDrawColor(22, 163, 74); + $this->SetFillColor(240, 253, 244); + $this->SetLineWidth(0.2); + $this->SetFont('Arial', 'B', 10); + $this->SetTextColor(21, 128, 61); + + $text = "Ce document confirme que votre paiement a bien été reçu et validé par Ludik Event.\nNous vous remercions de votre confiance."; + $this->MultiCell(0, 10, $this->clean($text), 1, 'C', true); + + // --- 4. SIGNATURE / CACHET --- + $this->SetY(-60); + $this->SetFont('Arial', 'B', 9); + $this->SetTextColor(0, 0, 0); + $this->Cell(0, 5, $this->clean('{{Sign;type=signature;role=Ludikevent;height=120;width=236}}'), 0, 1, 'C'); + } + public function Footer(): void + { + // Positionnement à 1,5 cm du bas + $this->SetY(-15); + $this->SetFont('Arial', 'I', 7); + $this->SetTextColor(150, 150, 150); + + // Texte gauche : Identification du devis + $this->Cell(0, 10, $this->clean('Confirmation de paiement Ludik Event - Lilian SEGARD - SIRET 93048840800012'), 0, 0, 'L'); + + + // Numérotation des pages à droite + $this->SetTextColor(150, 150, 150); + $this->SetFont('Arial', 'I', 8); + $this->Cell(0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'R'); + } +} diff --git a/src/Service/Signature/Client.php b/src/Service/Signature/Client.php index dcb99c1..31026ac 100644 --- a/src/Service/Signature/Client.php +++ b/src/Service/Signature/Client.php @@ -3,6 +3,7 @@ namespace App\Service\Signature; use App\Entity\Contrats; +use App\Entity\ContratsPayments; use App\Entity\CustomerOrder; use App\Entity\Devis; use Doctrine\ORM\EntityManagerInterface; @@ -269,4 +270,33 @@ class Client $submiter = $this->getSubmiter($signId); return $submiter['uuid']; // numéro de signature; } + + public function autoSignConfirmedPayment(ContratsPayments $contratsPayments) { + $relativeFileUrl = $this->storage->resolveUri($contratsPayments, 'paymentFile'); + $fileUrl = $this->baseUrl . $relativeFileUrl; + $submission = $this->docuseal->createSubmissionFromPdf([ + 'name' => 'Confirmaton de paiement N°' . $contratsPayments->getPaymentId(), // Correction : getNum() + 'send_email' => true, + 'documents' => [ + [ + 'name' => 'confirmation_paiement_' . $contratsPayments->getId() . '.pdf', // Correction : getNum() + 'file' => $fileUrl, + ], + ], + 'submitters' => [ + [ + 'role' => 'Ludikevent', + 'email' => 'contact@ludikevent.fr', + 'completed' => true, + 'fields' => [ + ['name'=>'Sign','default_value'=>$this->logoBase64()] + ] + ], + ], + ]); + $sub = $this->docuseal->getSubmission($submission['id']); + sleep(5); + return $sub['documents'][0]['url']; + + } } diff --git a/src/Service/Stripe/Client.php b/src/Service/Stripe/Client.php index dd546ee..52c09be 100644 --- a/src/Service/Stripe/Client.php +++ b/src/Service/Stripe/Client.php @@ -394,7 +394,6 @@ class Client */ public function checkWebhooks(\Symfony\Component\HttpFoundation\Request $request, string $configName): bool { - return true; // 1. Récupération de la config (Secret de signature) $config = $this->em->getRepository(StripeConfig::class)->findOneBy(['name' => $configName]); diff --git a/templates/reservation/contrat/view.twig b/templates/reservation/contrat/view.twig index 9e73766..d77468f 100644 --- a/templates/reservation/contrat/view.twig +++ b/templates/reservation/contrat/view.twig @@ -362,6 +362,7 @@