feat(paiement): Ajoute l'interface de paiement Stancer et la gestion du statut.

This commit is contained in:
Serreau Jovann
2025-07-30 14:56:27 +02:00
parent 3bd5831722
commit 439e12bc0d
13 changed files with 338 additions and 11 deletions

View File

@@ -14,3 +14,5 @@ header {
.opblock-section-header{
background: #f8adf8 !important;
}

View File

@@ -1,5 +1,8 @@
import './app.scss'
import * as Turbo from "@hotwired/turbo"
import {PaymentPage} from "./class/PaymentPage.js";
customElements.define('payment-page',PaymentPage,{extends:'button'})
document.addEventListener('DOMContentLoaded', () => {

View File

@@ -36,3 +36,37 @@ input {
height: 20px;
color: #6b7280; /* Tailwind gray-500 */
}
.banke{
background: red;
padding: 0.5rem;
width: 100%;
margin-bottom: 1rem;
display: none;
&.show{
display: block;
}
}
.modal-payment{
background: rgba(0,0,0,0.5);
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(5px);
.iframe-container {
background: var(--color-gray-800);
width: 50%;
height: 90%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
iframe {
width: 100%;
height: 100%;
}
}
}

View File

@@ -0,0 +1,47 @@
export class PaymentPage extends HTMLButtonElement{
connectedCallback(){
let element = this;
element.addEventListener('click', function(){
let banke = document.querySelector('.banke');
banke.classList.remove('show');
let displayModal = document.createElement('div');
displayModal.classList.add('modal-payment');
let iframeContainer = document.createElement('div');
iframeContainer.classList.add('iframe-container');
window.addEventListener('message', function (event) {
if (event.origin !== 'https://payment.stancer.com') {
return;
}
if(event.data.status != undefined) {
if(event.data.status != "init" && event.data.status != "pending" && event.data.status != "secure-auth-start" && event.data.status != "secure-auth-end") {
if (event.data.status == "paid") {
displayModal.remove();
location.href = "/paiement/complete?id=" + element.getAttribute('id');
} else if (event.data.status == "finished") {
displayModal.remove();
location.href = event.data.url;
} else {
fetch("/api-interne/intranet/customer/payment/cancel?id="+element.getAttribute('id'))
displayModal.remove();
banke.classList.add('show');
}
}
}
console.log(event.data);
});
fetch("/api-interne/intranet/customer/payment?id="+element.getAttribute('id'))
.then(r=>r.json())
.then(r=>{
let iframe = document.createElement('iframe');
iframe.src = r.url;
iframeContainer.appendChild(iframe);
window.frame = iframeContainer;
})
displayModal.appendChild(iframeContainer);
document.body.appendChild(displayModal);
})
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250730123638 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer_advert_payment ADD card TEXT DEFAULT NULL');
$this->addSql('ALTER TABLE customer_advert_payment ADD pay_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN customer_advert_payment.card IS \'(DC2Type:array)\'');
$this->addSql('COMMENT ON COLUMN customer_advert_payment.pay_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 customer_advert_payment DROP card');
$this->addSql('ALTER TABLE customer_advert_payment DROP pay_at');
}
}

View File

@@ -3,15 +3,88 @@
namespace App\Controller\ApiInterne\Intranet;
use App\Entity\CustomerAdvertPayment;
use App\Repository\CustomerAdvertPaymentRepository;
use Doctrine\ORM\EntityManagerInterface;
use LuFiipe\InseeSierene\Sirene;
use Stancer\Config;
use Stancer\Customer;
use Stancer\Payment;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class CustomerController extends AbstractController
{
#[Route(path: '/api-interne/intranet/customer/payment/cancel',name: 'api-interne-intranet-customer-payment-cancel')]
public function customerPaymentCancel(EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPayment): Response
{
if(!$request->query->has('id'))
return $this->json([],Response::HTTP_FORBIDDEN);
$advert = $customerAdvertPayment->find($request->query->get('id'));
if(!$advert instanceof CustomerAdvertPayment)
return $this->json([],Response::HTTP_FORBIDDEN);
if($advert->getState() == "pay")
return $this->json([],Response::HTTP_FORBIDDEN);
$advert->setPaymentId(null);
$entityManager->persist($advert);
$entityManager->flush();
return $this->json([]);
}
#[Route(path: '/api-interne/intranet/customer/payment/',name: 'api-interne-intranet-customer-payment')]
public function customerPayment(EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPayment): Response
{
if(!$request->query->has('id'))
return $this->json([],Response::HTTP_FORBIDDEN);
$advert = $customerAdvertPayment->find($request->query->get('id'));
if(!$advert instanceof CustomerAdvertPayment)
return $this->json([],Response::HTTP_FORBIDDEN);
if($advert->getState() == "pay")
return $this->json([],Response::HTTP_FORBIDDEN);
$payEdit = str_replace('"',"",$advert->getPaymentId());
$client = Config::init([$_ENV['STANCER_PUBLIC_KEY'], $_ENV['STANCER_PRIVATE_KEY']]);
$client->setMode($_ENV['STANCER_ENV']);
if($advert->getPaymentId() != null) {
$payment = new Payment($payEdit);
return $this->json([
'url' => $payment->getPaymentPageUrl(),
]);
} else {
$total = 0;
foreach ($advert->getCustomerAdvertPaymentLines() as $item) {
$total = $total + (floatval($item->getPriceHt()) * 1.20);
}
$paymentReturnPath = $this->generateUrl("app_payment_complete", ['id'=>$advert->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
$paymentReturnPath = str_replace("http://", "https://", $paymentReturnPath);
$customerStancer = new Customer($advert->getCustomer()->getStancerId());
$payment = new Payment();
$payment->setAmount($total * 100);
$payment->setCurrency("EUR");
$payment->setDescription("Paiement de l'avis de paiement - " . $advert->getNumAvis());
$payment->setCustomer($customerStancer);
$payment->setReturnUrl($paymentReturnPath);
$payment->setOrderId($advert->getNumAvis());
$payment->setMethodsAllowed(["card"]);
$payment->setCapture(true);
$paimentId = $payment->send();
$advert->setPaymentId($paimentId);
$entityManager->persist($advert);
$entityManager->flush();
}
return $this->json([
'url' => $payment->getPaymentPageUrl(),
]);
}
#[Route(path: '/api-interne/intranet/customer/auto/{siret}',name: 'api-interne-intranet-customer-auto')]
public function customerAuto(?string $siret): Response
{

View File

@@ -3,8 +3,12 @@
namespace App\Controller;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentLine;
use App\Repository\CustomerAdvertPaymentRepository;
use App\Service\Mailer\Mailer;
use Doctrine\ORM\EntityManagerInterface;
use Stancer\Config;
use Stancer\Customer;
use Stancer\Payment;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -13,14 +17,57 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Uid\Uuid;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class PaymentController extends AbstractController
{
#[Route(path: '/paiement/complete',name: 'app_payment_complete')]
public function paymentComplete(EntityManagerInterface $entityManager,Mailer $mailer,Request $request,CustomerAdvertPaymentRepository $customerAdvertPaymentRepository): Response
{
if(!$request->query->has('id'))
return $this->render('admin/payement_invalid.twig',[
'message' => 'Avis de paiement n\'a été trouver'
]);
$advert = $customerAdvertPaymentRepository->find($request->query->get('id'));
if(!$advert instanceof CustomerAdvertPayment)
return $this->render('admin/payement_invalid.twig',[
'message' => 'Avis de paiement n\'a été trouver'
]);
if($advert->getState() == "pay") {
$client = Config::init([$_ENV['STANCER_PUBLIC_KEY'], $_ENV['STANCER_PRIVATE_KEY']]);
$client->setMode($_ENV['STANCER_ENV']);
$payEdit = str_replace('"', "", $advert->getPaymentId());
$payment = new Payment($payEdit);
if ($payment->isSuccess()) {
$cardInfo = [
'brand' => $payment->card->brand,
'brandName' => $payment->card->brandName,
'exp_year' => $payment->card->exp_year,
'exp_month' => $payment->card->exp_month,
'last4' => $payment->card->last4,
'country' => $payment->card->country,
];
$advert->setState("pay");
$advert->setCard($cardInfo);
$advert->setPayAt(new \DateTimeImmutable());
$entityManager->persist($advert);
$entityManager->flush();
dd('email customer');
dd('email siteconseil');
}
}
return $this->render('admin/payement_complete.twig',[
'advert' => $advert,
]);
}
#[Route(path: '/paiement',name: 'app_payment')]
public function signComplete(Request $request,CustomerAdvertPaymentRepository $customerAdvertPaymentRepository): Response
public function payment(EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPaymentRepository): Response
{
if(!$request->query->has('id'))
return $this->render('admin/payement_invalid.twig',[
@@ -53,10 +100,34 @@ class PaymentController extends AbstractController
'number' =>$advert->getNumAvis(),
'amount' => $amount,
'amountHt' => $amountHt,
'payment' =>$payment->getPaymentPageUrl(),
];
/* if($request->query->has('act') && $request->query->get('act') == 'createPaiment'){
$total = 0;
foreach ($advert->getCustomerAdvertPaymentLines() as $item) {
$total = $total + (floatval($item->getPriceHt()) * 1.20);
}
$paymentReturnPath = $this->generateUrl("app_payment_complete", ['id'=>$advert->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
$paymentReturnPath = str_replace("http://", "https://", $paymentReturnPath);
$customerStancer = new Customer($advert->getCustomer()->getStancerId());
$payment = new Payment();
$payment->setAmount($total * 100);
$payment->setCurrency("EUR");
$payment->setDescription("Paiement de l'avis de paiement - " . $advert->getNumAvis());
$payment->setCustomer($customerStancer);
$payment->setReturnUrl($paymentReturnPath);
$payment->setOrderId($advert->getNumAvis());
$payment->setMethodsAllowed(["card"]);
$payment->setCapture(true);
$paimentId = $payment->send();
$advert->setPaymentId($paimentId);
$entityManager->persist($advert);
$entityManager->flush();
return $this->redirect($payment->getPaymentPageUrl());
}*/
return $this->render('admin/payement_interface.twig',[
'paymentNotice' => $paymentNotice
'paymentNotice' => $paymentNotice,
'advert' => $advert
]);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\CustomerAdvertPaymentRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
@@ -60,6 +61,12 @@ class CustomerAdvertPayment
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
#[ORM\Column(type: Types::ARRAY,nullable: true)]
private array $card = [];
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $payAt = null;
public function __construct()
{
$this->customerAdvertPaymentLines = new ArrayCollection();
@@ -308,4 +315,28 @@ class CustomerAdvertPayment
{
$this->fileOriginalName = $fileOriginalName;
}
public function getCard(): array
{
return $this->card;
}
public function setCard(array $card): static
{
$this->card = $card;
return $this;
}
public function getPayAt(): ?\DateTimeImmutable
{
return $this->payAt;
}
public function setPayAt(\DateTimeImmutable $payAt): static
{
$this->payAt = $payAt;
return $this;
}
}

View File

@@ -70,7 +70,7 @@ class BillingEventSusbriber
$this->entityManager->flush();
if($createAvis->getPaymentId() == null) {
$paymentReturnPath = $this->urlGenerator->generate("app_login", [], UrlGeneratorInterface::ABSOLUTE_URL);
$paymentReturnPath = $this->urlGenerator->generate("app_payment_complete", ['id'=>$createAvis->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
$paymentReturnPath = str_replace("http://", "https://", $paymentReturnPath);
$total = 0;
@@ -78,8 +78,8 @@ class BillingEventSusbriber
foreach ($createAvis->getCustomerAdvertPaymentLines() as $item) {
$total = $total + (floatval($item->getPriceHt()) * 1.20);
}
$customerStancer = new Customer($customerId);
//creeat payement link
//$customerStancer = new Customer($customerId);
/*//creeat payement link
$payment = new Payment();
$payment->setAmount($total * 100);
$payment->setCurrency("EUR");
@@ -92,7 +92,7 @@ class BillingEventSusbriber
$paimentId = $data = $payment->send();
$createAvis->setPaymentId($paimentId);
$this->entityManager->persist($createAvis);
$this->entityManager->flush();
$this->entityManager->flush();*/
}
$pdf = New PaymentPdf($this->kernel,$createAvis,$this->urlGenerator->generate('app_payment',[

View File

@@ -0,0 +1,27 @@
{% extends 'admin/base.twig' %}
{% block content %}
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8 p-10 rounded-xl shadow-lg bg-gray-700">
<div class="text-center">
<!-- Icône de succès (SVG ou Emoji) -->
<svg class="mx-auto h-24 w-24 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h2 class="mt-6 text-3xl font-extrabold text-white">
Paiement effectué avec succès !
</h2>
<p class="mt-2 text-sm text-white">
Merci pour votre paiement. Votre transaction a été traitée avec succès.
</p>
<p class="mt-2 text-sm text-white-600">
Une fois le paiement vérifiée de notre coté, une facture vous sera envoyée
</p>
</div>
</div>
</div>
{% endblock %}
{% block title %}
Paiement réussi !
{% endblock %}

View File

@@ -4,6 +4,8 @@
<div class="container mx-auto p-6 bg-gray-800 shadow-md rounded-lg">
<h2 class="text-2xl font-bold mb-6 text-white">Détails de votre avis de paiement</h2>
<span class=" bg-red-900 banke text-white">Votre paiement à été refusée par votre banque</span>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-500 p-4 rounded-md">
<p class="text-sm font-medium text-white-600">Client</p>
@@ -24,12 +26,12 @@
</div>
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
<a href="" class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md text-center transition duration-300 ease-in-out">
<a target="_blank" href="{{ vich_uploader_asset(advert,'file') }}" class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md text-center transition duration-300 ease-in-out">
Voir l'avis de paiement
</a>
<a href="{{ paymentNotice.payment }}" class="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-md text-center transition duration-300 ease-in-out">
<button is="payment-page" id="{{ advert.id }}" class="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-md text-center transition duration-300 ease-in-out">
Payer
</a>
</button>
</div>
</div>
{% endblock %}

View File

@@ -17,7 +17,7 @@
<td class="px-6 py-4">{{ orderAdvert.numAvis }}</td>
<td class="px-6 py-4">{{ orderAdvert.createAt|date('d/m/Y H:i') }}</td>
<td class="px-6 py-4">{{ (orderAdvert|totalOrder)|format_currency('EUR') }}</td>
<td class="px-6 py-4 {% if orderAdvert.state == "accepted"%} text-green-400 {% elseif orderAdvert.state == "cancel"%} text-red-400{% else %}text-orange-400{% endif %}">{{ orderAdvert.state|trans }} </td>
<td class="px-6 py-4 {% if orderAdvert.state == "pay"%} text-green-400 {% else %}text-orange-400{% endif %}">{{ orderAdvert.state|trans }} </td>
<td class="px-6 py-4 text-center">
{% if orderAdvert.state == "created" %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,currentOrder:'a',current:'order',idAvis:orderAdvert.id,act:'send'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Envoyée l'avis de paiement</a>

View File

@@ -14,3 +14,4 @@ send: Envoyée - Attends de signature
send_avis: Envoyée - Attends de paiement
accepted: Accéptée
cancel: Annulée
pay: Payée