feat(SignController): Ajoute la gestion des signatures Docuseal et les notifications.

This commit is contained in:
Serreau Jovann
2025-07-29 11:10:25 +02:00
parent 369877742d
commit 358688eb5d
22 changed files with 1066 additions and 25 deletions

2
.env
View File

@@ -58,5 +58,5 @@ OVH_KEY=34bc2c2eb416b67d
OVH_SECRET=12239d273975b5ab53318907fb66d355
OVH_CUSTOMER=56c387eb9ca4b9a2de4d4d97fd3d7f22
DOCUSIGN_URL=signature.esy-web.dev
DOCUSIGN_URL=https://signature.esy-web.dev/api
DOCUSIGN_KEY=52u82oCoiG79awGsuxLfJqhxYjg8mrJfAsJJHejRMFa

View File

@@ -7,11 +7,12 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"chillerlan/php-qrcode": "*",
"doctrine/dbal": "^3.10",
"doctrine/doctrine-bundle": "^2.15",
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.5",
"docusealco/docuseal-php": "^1.0",
"docusealco/docuseal-php": "*",
"fpdf/fpdf": "*",
"google/cloud": "^0.296.0",
"imagine/imagine": "^1.5",

161
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0d15b0c5ba47b24aa16e0727699f4ab3",
"content-hash": "1d76d59e951b53e55af5bbeb42af9bae",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -217,6 +217,165 @@
],
"time": "2025-03-29T13:50:30+00:00"
},
{
"name": "chillerlan/php-qrcode",
"version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/42e215640e9ebdd857570c9e4e52245d1ee51de2",
"reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"chillerlan/php-authenticator": "^4.3.1 || ^5.2.1",
"ext-fileinfo": "*",
"phan/phan": "^5.4.5",
"phpcompatibility/php-compatibility": "10.x-dev",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"slevomat/coding-standard": "^8.15",
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT",
"Apache-2.0"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase/qrcode-generator"
},
{
"name": "ZXing Authors",
"homepage": "https://github.com/zxing/zxing"
},
{
"name": "Ashot Khanamiryan",
"homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"description": "A QR Code generator and reader with a user-friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"keywords": [
"phpqrcode",
"qr",
"qr code",
"qr-reader",
"qrcode",
"qrcode-generator",
"qrcode-reader"
],
"support": {
"docs": "https://php-qrcode.readthedocs.io",
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode"
},
"funding": [
{
"url": "https://ko-fi.com/codemasher",
"type": "Ko-Fi"
}
],
"time": "2024-11-21T16:12:34+00:00"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"keywords": [
"Settings",
"configuration",
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"time": "2024-07-16T11:13:48+00:00"
},
{
"name": "composer/semver",
"version": "3.4.3",

View File

@@ -17,6 +17,22 @@ vich_uploader:
inject_on_load: false
delete_on_update: true
delete_on_remove: true
devis_sign:
uri_prefix: /storage/devis_sign
upload_destination: '%kernel.project_dir%/public/storage/devis_sign'
namer: App\VichUploader\Namer\Customer\DevisName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Customer\DevisName
inject_on_load: false
delete_on_update: true
delete_on_remove: true
devis_audit:
uri_prefix: /storage/devis_audit
upload_destination: '%kernel.project_dir%/public/storage/devis_audit'
namer: App\VichUploader\Namer\Customer\DevisName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Customer\DevisName
inject_on_load: false
delete_on_update: true
delete_on_remove: true
advert_payment:
uri_prefix: /storage/advert_payment
upload_destination: '%kernel.project_dir%/public/storage/advert_payment'

View File

@@ -0,0 +1,33 @@
<?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 Version20250729073622 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('CREATE TABLE sign_event (id SERIAL NOT NULL, submiter_event INT NOT NULL, state VARCHAR(255) NOT NULL, action_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN sign_event.action_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('DROP TABLE sign_event');
}
}

View File

@@ -0,0 +1,50 @@
<?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 Version20250729082955 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_devis ADD devis_audit_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_audit_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_audit_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_audit_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_audit_original_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_sign_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_sign_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_sign_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_sign_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_devis ADD devis_sign_original_name VARCHAR(255) DEFAULT NULL');
}
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_devis DROP devis_audit_file_name');
$this->addSql('ALTER TABLE customer_devis DROP devis_audit_dimensions');
$this->addSql('ALTER TABLE customer_devis DROP devis_audit_size');
$this->addSql('ALTER TABLE customer_devis DROP devis_audit_mine_type');
$this->addSql('ALTER TABLE customer_devis DROP devis_audit_original_name');
$this->addSql('ALTER TABLE customer_devis DROP devis_sign_file_name');
$this->addSql('ALTER TABLE customer_devis DROP devis_sign_dimensions');
$this->addSql('ALTER TABLE customer_devis DROP devis_sign_size');
$this->addSql('ALTER TABLE customer_devis DROP devis_sign_mine_type');
$this->addSql('ALTER TABLE customer_devis DROP devis_sign_original_name');
}
}

View File

@@ -133,8 +133,6 @@ class CustomerController extends AbstractController
$orderDevis = $entityManager->getRepository(CustomerDevis::class)->findBy(['customer'=>$customer],['id'=>'ASC']);
$event = new CreateDevisCustomerEvent($orderDevis[0],true);
$eventDispatcher->dispatch($event);
return $this->render('artemis/intranet/customer/edit.twig',[
'form' => $form->createView(),

View File

@@ -111,7 +111,7 @@ class AccountController extends AbstractController
return $this->redirectToRoute('artemis_settings_accountAdmin_view',['id'=>$account->getId()]);
}
$lastLogin = $accountLoginRegisterRepository->lastLogin($account);
$account->lastLoginAt = $lastLogin[0];
$account->lastLoginAt = $lastLogin[0] ?? new AccountLoginRegister();
$formPassword = $this->createForm(AdminPasswordType::class);
$formPassword->handleRequest($request);

View File

@@ -34,6 +34,7 @@ class HomeController extends AbstractController
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
#[Route(path: '/logout',name: 'app_logout',methods: ['GET', 'POST'])]
public function logout(AuthenticationUtils $authenticationUtils): Response
{

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Controller;
use App\Entity\CustomerDevis;
use App\Entity\SignEvent;
use App\Repository\CustomerDevisRepository;
use App\Service\Mailer\Mailer;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Psr7\UploadedFile;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
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\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Uid\Uuid;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class SignController extends AbstractController
{
#[Route(path: '/sign-complete',name: 'app_sign_complete')]
public function signComplete(UploaderHelper $uploaderHelper,Request $request,CustomerDevisRepository $customerDevisRepository): Response
{
$document = [];
$type = $request->get('type');
$id = $request->get('id');
if($type == "devis") {
$object = $customerDevisRepository->find($id);
$document =[
'number' => $object->getNumDevis(),
'signatureDate' => $object->getSignAt(),
'signer' => $object->getCustomer()->mainContact()->getName()." ". $object->getCustomer()->mainContact()->getSurname(),
'path' => $uploaderHelper->asset($object,"devis"),
'pathSign' => $uploaderHelper->asset($object,"devisSign"),
];
}
return $this->render('admin/sign-complete.twig',[
'document' => $document
]);
}
#[Route(path: '/webhook/sign',name: 'app_webhook_sign')]
public function webhookSign(KernelInterface $kernel,UploaderHelper $uploaderHelper,Mailer $mailer,EntityManagerInterface $entityManager,Request $request): Response
{
if(!$request->headers->has('X-Sign'))
return $this->json([],Response::HTTP_BAD_REQUEST);
if($request->headers->has('X-Sign') != "SignMainframe")
return $this->json([],Response::HTTP_BAD_REQUEST);
$content = $request->getContent();
$content = json_decode($content);
$event_type = $content->event_type;
$timestamp = $content->timestamp;
if($event_type == "form.declined") {
$metadata = $content->data->metadata;
if($metadata->type == "devis") {
/** @var CustomerDevis $devis */
$devis = $entityManager->getRepository(CustomerDevis::class)->find($metadata->id);
$devis->setState("declined - ".$content->data->decline_reason);
$entityManager->persist($devis);
$entityManager->flush();
}
}
if($event_type == "form.viewed") {
$submittersId = $content->data->submission_id;
$sign = new SignEvent();
$sign->setSubmiterEvent($submittersId);
$sign->setState($event_type);
$sign->setActionAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z',$timestamp));
$entityManager->persist($sign);
$entityManager->flush();
}
if($event_type == "submission.completed") {
$data = $content->data;
$submitters = $data->submitters;
$submittersId = $submitters[0]->id;
$sign = new SignEvent();
$sign->setSubmiterEvent($submittersId);
$sign->setState( "submission.completed");
$sign->setActionAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z',$timestamp));
//$entityManager->persist($sign);
//$entityManager->flush();
//audit file
$auditLogUrl = $content->data->audit_log_url;
$documentUrl = $content->data->submitters[0]->documents[0]->url;
$auditContent = file_get_contents($auditLogUrl);
$signContent = file_get_contents($documentUrl);
$tmpAuditName = "audit-".Uuid::v4().".pdf";
$dirAudit = sys_get_temp_dir().'/'.$tmpAuditName;
$tmpSignName = "sign-".Uuid::v4().".pdf";
$dirSign = sys_get_temp_dir().'/'.$tmpSignName;
file_put_contents($dirAudit,$auditContent);
file_put_contents($dirSign,$signContent);
$metadata = $content->data->submitters[0]->metadata;
if($metadata->type == "devis") {
/** @var CustomerDevis $devis */
$devis = $entityManager->getRepository(CustomerDevis::class)->find($metadata->id);
$devis->setState("accepted");
$uploadFileSign = new \Symfony\Component\HttpFoundation\File\UploadedFile($dirSign,"Signé - ".$devis->getNumDevis().".pdf","application/pdf",0,true);
$uploadFileAudit = new \Symfony\Component\HttpFoundation\File\UploadedFile($dirAudit,"Audit - ".$devis->getNumDevis().".pdf","application/pdf",0,true);
$devis->setDevisSign($uploadFileSign);
$devis->setDevisAudit($uploadFileAudit);
$devis->setSignAt(new \DateTimeImmutable());
$entityManager->persist($devis);
$entityManager->flush();
$files =[];
$files[] = new DataPart(file_get_contents($kernel->getProjectDir()."/public".$uploaderHelper->asset($devis,"devis")),"Devis ".$devis->getNumDevis().".pdf");
$files[] = new DataPart($signContent,"Attestation de signature ".$devis->getNumDevis()." - devis.pdf");
$mailer->send($devis->getCustomer()->mainContact()->getEmail(),$devis->getCustomer()->getRaisonSocial(),"[SARL SITECONSEIL] - Signature de votre de devis","mails/customer/devis-sign.twig",[
'devis' => $devis,
'customer' => $devis->getCustomer(),
'contact' => $devis->getCustomer()->mainContact(),
],$files);
$mailer->send("s.com@siteconseil.fr","SARL SITECONSEIL","[SARL SITECONSEIL] - Signature du devis","mails/customer/devis-sign-customer.twig",[
'devis' => $devis,
'customer' => $devis->getCustomer(),
'contact' => $devis->getCustomer()->mainContact(),
]);
}
}
if($event_type == "submission.created") {
$data = $content->data;
$submitters = $data->submitters;
$submittersId = $submitters[0]->id;
$sign = new SignEvent();
$sign->setSubmiterEvent($submittersId);
$sign->setState( "submission.created");
$sign->setActionAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z',$timestamp));
$entityManager->persist($sign);
$entityManager->flush();
}
return $this->json([]);
}
}

View File

@@ -48,6 +48,32 @@ class CustomerDevis
#[ORM\OneToMany(targetEntity: CustomerDevisLine::class, mappedBy: 'devis')]
private Collection $customerDevisLines;
#[Vich\UploadableField(mapping: 'devis_audit',fileNameProperty: 'devisAuditFileName', size: 'devisAuditSize', mimeType: 'devisAuditMineType', originalName: 'devisAuditOriginalName',dimensions: 'devisAuditDimensions')]
private ?File $devisAudit = null;
#[ORM\Column(nullable: true)]
private ?string $devisAuditFileName = null;
#[ORM\Column(nullable: true)]
private ?array $devisAuditDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisAuditSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisAuditMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisAuditOriginalName = null;
#[Vich\UploadableField(mapping: 'devis_sign',fileNameProperty: 'devisSignFileName', size: 'devisSignSize', mimeType: 'devisSignMineType', originalName: 'devisSignOriginalName',dimensions: 'devisSignDimensions')]
private ?File $devisSign = null;
#[ORM\Column(nullable: true)]
private ?string $devisSignFileName = null;
#[ORM\Column(nullable: true)]
private ?array $devisSignDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisSignSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisSignMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $devisSignOriginalName = null;
#[Vich\UploadableField(mapping: 'devis',fileNameProperty: 'devisFileName', size: 'devisSize', mimeType: 'devisMineType', originalName: 'devisOriginalName',dimensions: 'devisDimensions')]
private ?File $devis = null;
@@ -330,4 +356,210 @@ class CustomerDevis
$this->devisOriginalName = $devisOriginalName;
}
/**
* @param File|null $devisAudit
*/
public function setDevisAudit(?File $devisAudit): void
{
$this->devisAudit = $devisAudit;
if($devisAudit !== null) {
$this->updateAt = new \DateTimeImmutable();
}
}
/**
* @param File|null $devisSign
*/
public function setDevisSign(?File $devisSign): void
{
$this->devisSign = $devisSign;
if($devisSign !== null) {
$this->updateAt = new \DateTimeImmutable();
}
}
/**
* @return File|null
*/
public function getDevisAudit(): ?File
{
return $this->devisAudit;
}
/**
* @return File|null
*/
public function getDevisSign(): ?File
{
return $this->devisSign;
}
/**
* @param array|null $devisAuditDimensions
*/
public function setDevisAuditDimensions(?array $devisAuditDimensions): void
{
$this->devisAuditDimensions = $devisAuditDimensions;
}
/**
* @param string|null $devisAuditFileName
*/
public function setDevisAuditFileName(?string $devisAuditFileName): void
{
$this->devisAuditFileName = $devisAuditFileName;
}
/**
* @param string|null $devisAuditMineType
*/
public function setDevisAuditMineType(?string $devisAuditMineType): void
{
$this->devisAuditMineType = $devisAuditMineType;
}
/**
* @param Collection $customerDevisLines
*/
public function setCustomerDevisLines(Collection $customerDevisLines): void
{
$this->customerDevisLines = $customerDevisLines;
}
/**
* @param string|null $devisAuditOriginalName
*/
public function setDevisAuditOriginalName(?string $devisAuditOriginalName): void
{
$this->devisAuditOriginalName = $devisAuditOriginalName;
}
/**
* @param array|null $devisSignDimensions
*/
public function setDevisSignDimensions(?array $devisSignDimensions): void
{
$this->devisSignDimensions = $devisSignDimensions;
}
/**
* @param string|null $devisAuditSize
*/
public function setDevisAuditSize(?string $devisAuditSize): void
{
$this->devisAuditSize = $devisAuditSize;
}
/**
* @param string|null $devisSignFileName
*/
public function setDevisSignFileName(?string $devisSignFileName): void
{
$this->devisSignFileName = $devisSignFileName;
}
/**
* @param string|null $devisSignMineType
*/
public function setDevisSignMineType(?string $devisSignMineType): void
{
$this->devisSignMineType = $devisSignMineType;
}
/**
* @param string|null $devisSignOriginalName
*/
public function setDevisSignOriginalName(?string $devisSignOriginalName): void
{
$this->devisSignOriginalName = $devisSignOriginalName;
}
/**
* @param string|null $devisSignSize
*/
public function setDevisSignSize(?string $devisSignSize): void
{
$this->devisSignSize = $devisSignSize;
}
/**
* @return array|null
*/
public function getDevisAuditDimensions(): ?array
{
return $this->devisAuditDimensions;
}
/**
* @return string|null
*/
public function getDevisAuditFileName(): ?string
{
return $this->devisAuditFileName;
}
/**
* @return string|null
*/
public function getDevisAuditMineType(): ?string
{
return $this->devisAuditMineType;
}
/**
* @return string|null
*/
public function getDevisAuditOriginalName(): ?string
{
return $this->devisAuditOriginalName;
}
/**
* @return string|null
*/
public function getDevisAuditSize(): ?string
{
return $this->devisAuditSize;
}
/**
* @return array|null
*/
public function getDevisSignDimensions(): ?array
{
return $this->devisSignDimensions;
}
/**
* @return string|null
*/
public function getDevisSignFileName(): ?string
{
return $this->devisSignFileName;
}
/**
* @return string|null
*/
public function getDevisSignMineType(): ?string
{
return $this->devisSignMineType;
}
/**
* @return string|null
*/
public function getDevisSignOriginalName(): ?string
{
return $this->devisSignOriginalName;
}
/**
* @return string|null
*/
public function getDevisSignSize(): ?string
{
return $this->devisSignSize;
}
}

65
src/Entity/SignEvent.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Repository\SignEventRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: SignEventRepository::class)]
class SignEvent
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $submiterEvent = null;
#[ORM\Column(length: 255)]
private ?string $state = null;
#[ORM\Column]
private ?\DateTimeImmutable $actionAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getSubmiterEvent(): ?int
{
return $this->submiterEvent;
}
public function setSubmiterEvent(int $submiterEvent): static
{
$this->submiterEvent = $submiterEvent;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getActionAt(): ?\DateTimeImmutable
{
return $this->actionAt;
}
public function setActionAt(\DateTimeImmutable $actionAt): static
{
$this->actionAt = $actionAt;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\SignEvent;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<SignEvent>
*/
class SignEventRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SignEvent::class);
}
// /**
// * @return SignEvent[] Returns an array of SignEvent objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('s.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?SignEvent
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -3,17 +3,21 @@
namespace App\Service\Customer;
use App\Service\Customer\Billing\CreateDevisCustomerEvent;
use App\Service\Docuseal\SignClient;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\DevisPdf;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Uid\Uuid;
#[AsEventListener(event: CreateDevisCustomerEvent::class, method: 'onBillingEvent')]
class BillingEventSusbriber
{
public function __construct(private readonly EntityManagerInterface $entityManager,private KernelInterface $kernel)
public function __construct(private readonly Mailer $mailer,private readonly SignClient $signClient,private readonly EntityManagerInterface $entityManager,private KernelInterface $kernel)
{
}
@@ -25,14 +29,28 @@ class BillingEventSusbriber
$pdf = New DevisPdf($this->kernel,$devis);
$dir =tempnam(sys_get_temp_dir(),"dv-").".pdf";
$pdf->Output($dir,'F');
$tmpname = Uuid::v4().".pdf";
$dir = sys_get_temp_dir().'/'.$tmpname;
$pdf->generate();
$content = $pdf->Output('S');
file_put_contents($dir,$content);
$upload = new UploadedFile($dir,"devis-".$devis->getNumDevis().".pdf","application/pdf",0,true);
$devis->setDevis($upload);
$this->entityManager->persist($devis);
$this->entityManager->flush();
if($send) {
$files =[];
$files[] = new DataPart($content,"Devis ".$devis->getNumDevis().".pdf");
$url = $this->signClient->createSubmissionDevis($content,$devis);
$this->mailer->send($devis->getCustomer()->mainContact()->getEmail(),$devis->getCustomer()->getRaisonSocial(),"[SARL SITECONSEIL] - Nouveaux devis à signée","mails/customer/devis-wait.twig",[
'devis' => $devis,
'customer' => $devis->getCustomer(),
'url' => $url,
],$files);
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Service\Docuseal;
use App\Entity\CustomerDevis;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SignClient
{
private \Docuseal\Api $docuseal;
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly EntityManagerInterface $entityManager)
{
$this->docuseal = new \Docuseal\Api($_ENV['DOCUSIGN_KEY'], $_ENV['DOCUSIGN_URL']);
}
public function createSubmissionDevis(string $content,CustomerDevis $devis)
{
$t = new \DateTimeImmutable();
if($devis->getDevisSubmiterId() == null) {
$submissionId = $this->docuseal->createSubmission([
'template_id' => 1,
'send_email' => false,
'completed_redirect_url' => $this->urlGenerator->generate('app_sign_complete',['type'=>'devis','id'=>$devis->getId()],UrlGeneratorInterface::ABSOLUTE_URL),
'submitters' => [
[
'role' => 'Client',
'email' => $devis->getCustomer()->mainContact()->getEmail(),
'metadata' => [
'type' =>'devis',
'id' => $devis->getId(),
],
'fields' => [
[
'name' => 'Numéro devis',
'value' => $devis->getNumDevis(),
'readonly' => true,
],
[
'name' => 'Raison Social',
'value' => $devis->getCustomer()->getRaisonSocial(),
'readonly' => true,
],
[
'name' => 'Adresse',
'value' => $devis->getCustomer()->getAddress(),
'readonly' => true,
],
[
'name' => 'Email',
'value' => $devis->getCustomer()->mainContact()->getEmail(),
'readonly' => true,
],
[
'name' => 'Date Signature',
'value' => 'Signée le ' . $t->format('d/m/Y H:i:s'),
'readonly' => true,
]
]
]
],
]);
$devis->setDevisSubmiterId($submissionId['id']);
$this->entityManager->persist($devis);
$this->entityManager->flush();
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
} else {
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
}
return "https://signature.esy-web.dev/s/".$submissionData['slug'];
}
}

View File

@@ -87,7 +87,7 @@ class Mailer
}
public function send(string $address, string $addressName, string $subject, string $template, array $data)
public function send(string $address, string $addressName, string $subject, string $template, array $data,array $files = [])
{
$dest = new Address($address, $addressName);
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
@@ -110,6 +110,9 @@ class Mailer
]);
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
$object->setContent($htmlContent);
foreach ($files as $file) {
$mail->addPart($file);
}
$mail->html($htmlContent);
try {

View File

@@ -2,8 +2,10 @@
namespace App\Service\Pdf;
use App\Entity\CustomerDevis;
use chillerlan\QRCode\QRCode;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
define('EURO',chr(128));
class DevisPdf extends FPDF
{
@@ -12,19 +14,18 @@ class DevisPdf extends FPDF
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerDevis $customerDevis)
{
parent::__construct();
$items = [
[
'title' => 'Produit 1',
'description' => 'Description détaillée du produit 1. Cette description est assez longue pour nécessiter plusieurs lignes dans le tableau.',
'unitPrice' => 10.00
],
[
'title' => 'Produit 2',
'description' => 'Description détaillée du produit 2. Cette description est également assez longue pour nécessiter plusieurs lignes dans le tableau.',
'unitPrice' => 15.00
],
];
$items = [];
foreach ($this->customerDevis->getCustomerDevisLines() as $line) {
$items[$line->getPos()] =[
'title' => $line->getName(),
'content' =>$line->getContent(),
'priceHt' => $line->getPriceHT(),
'priceTTC' => (1.20*$line->getPriceHT()),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Devis N° ","ISO-8859-1","UTF-8").$this->customerDevis->getNumDevis());
}
function Header()
@@ -43,6 +44,23 @@ class DevisPdf extends FPDF
$this->SetFont('Arial', 'B', 12);
$this->Text(150, 10, mb_convert_encoding("DEVIS N° ".$this->customerDevis->getNumDevis(), 'ISO-8859-1', 'UTF-8'));
$this->Text(150, 15, mb_convert_encoding("Date: ".$this->customerDevis->getCreateAt()->format('d/m/Y'), 'ISO-8859-1', 'UTF-8'));
$y = 40;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
if($this->customerDevis->getCustomer()->getAddress2()!="") {
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
}
if($this->customerDevis->getCustomer()->getAddress3() != "") {
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
}
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode()." ".$this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$this->body();
}
function Footer()
@@ -55,4 +73,73 @@ class DevisPdf extends FPDF
$this->Ln(5);
$this->Cell(0, 5, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
public function body()
{
$this->Line(120,70,120,220);
$this->Line(160,70,160,220);
$this->Text(125,70,mb_convert_encoding("PRIX HT","ISO-8859-1","UTF-8"));
$this->Text(165,70,mb_convert_encoding("PRIX TTC","ISO-8859-1","UTF-8"));
}
function generate()
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
foreach($this->items as $item) {
if ($this->GetY() + 30 > $this->PageBreakTrigger) {
$this->AddPage();
$this->body();
}
$this->SetY($this->GetPageHeight() / 3.75);
$this->SetFont('Arial', 'B', 12);
$this->Cell(100,10,mb_convert_encoding($item['title'],'ISO-8859-1','UTF-8'),0,0);
$this->SetFont('Arial', '', 12);
$this->SetX($this->GetX()+12);
$this->Cell(35,10,number_format($item['priceHt'],2,",")." ".EURO,0);
$this->SetX($this->GetX()+5);
$this->Cell(35,10,number_format($item['priceTTC'],2,",")." ".EURO,0,true);
$this->SetX($this->GetX());
$this->MultiCell(100, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0);
}
$this->displaySummary();
}
function displaySummary()
{
$signTag="{{Signée Ici;type=signature}}";
$signDateTag="{{Date;type=date}}";
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20; // Assuming TVA is 20%
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60); // Adjust this value as needed
// Move to the left for signature and date tags
// Add signature and date tags
$this->Text(10,$this->GetY()-10, mb_convert_encoding('Pour accepter le devis, signez ici :', 'ISO-8859-1', 'UTF-8'));
$this->Text(10,$this->GetY(), mb_convert_encoding($signDateTag, 'ISO-8859-1', 'UTF-8'));
$this->Text(10,$this->GetY()+10, mb_convert_encoding($signTag, 'ISO-8859-1', 'UTF-8'));
// Display the summary
$this->SetFont('Arial', 'B', 12);
$this->Cell(150, 10, mb_convert_encoding('Total HT:', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalHT, 2, ",") . " " . EURO, 0, 1, 'R');
$this->Cell(150, 10, mb_convert_encoding('TVA (20%):', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTVA, 2, ",") . " " . EURO, 0, 1, 'R');
$this->Cell(150, 10, mb_convert_encoding('Total TTC:', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTTC, 2, ",") . " " . EURO, 0, 1, 'R');
}
}

View File

@@ -14,6 +14,6 @@ class DevisName implements NamerInterface
public function name(object $object, PropertyMapping $mapping): string
{
return $object->getNumDevis().".pdf";
return sprintf('%05d',$object->getId()).".pdf";
}
}

View File

@@ -0,0 +1,50 @@
{% extends 'admin/base.twig' %}
{% block title %}Confirmation de Signature{% endblock %}
{% block content %}
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
<h2 class="text-center text-3xl font-extrabold text-white">
Confirmation de Signature
</h2>
<div class="mt-8 rounded-lg p-6 shadow-md">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<h3 class="mt-2 text-xl font-medium text-white-900">
Votre document a été signé avec succès
</h3>
<p class="mt-1 text-white-600">
Merci pour votre confirmation. Vous trouverez ci-dessous les détails de votre document signé.
</p>
</div>
<div class="mt-6">
<div class="border-t border-gray-200 pt-4">
<dl class="divide-y divide-gray-200">
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium text-white-500">Numéro de document</dt>
<dd class="mt-1 text-sm text-white-900 sm:mt-0 sm:col-span-2">{{ document.number }}</dd>
</div>
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium text-white-500">Date de signature</dt>
<dd class="mt-1 text-sm text-white-900 sm:mt-0 sm:col-span-2">{{ document.signatureDate|date('d/m/Y H:i') }}</dd>
</div>
<div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium text-white-500">Signataire</dt>
<dd class="mt-1 text-sm text-white-900 sm:mt-0 sm:col-span-2">{{ document.signer }}</dd>
</div>
</dl>
</div>
</div>
<div class="mt-6 flex justify-center">
<a href="{{ document.path }}" download="" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Télécharger le document
</a>
<a href="{{ document.pathSign }}" download="" class="ml-2 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Télécharger le document signée
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-text>
Bonjour,
</mj-text>
<mj-text>
Nous vous informons que le client {{ datas.customer.raisonSocial }} a signé son devis numéro {{ datas.devis.numDevis }}.
</mj-text>
<mj-text>
Merci de bien vouloir procéder aux étapes comptables nécessaires.
</mj-text>
<mj-text>
Cordialement
</mj-text>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-text>
Bonjour,
</mj-text>
<mj-text>
Nous vous confirmons la signature de votre devis numéro {{ datas.devis.numDevis }}. Vous trouverez ci-joint le devis ainsi que l'attestation de signature.
</mj-text>
<mj-text>
Merci pour votre confiance.
</mj-text>
<mj-text>
Cordialement,<br />
L'équipe
</mj-text>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-text>
Bonjour,
</mj-text>
<mj-text>
Nous vous informons qu'un nouveau devis numéro {{ datas.devis.numDevis }} est prêt et attend votre signature. Vous trouverez ci-joint le devis en pièce jointe à cet email.
</mj-text>
<mj-button href="{{ datas.url }}" background-color="#4CAF50" color="white" font-family="Helvetica, Arial, sans-serif" font-size="16px" font-weight="bold" inner-padding="10px 25px" border-radius="3px">
Signer le devis
</mj-button>
<mj-text>
Merci de procéder à la signature dès que possible. Si vous avez des questions ou besoin d'assistance, n'hésitez pas à nous contacter.
</mj-text>
<mj-text>
Cordialement
</mj-text>
{% endblock %}