✨ feat(various): Refactorise et améliore plusieurs services et entités.
Supprime des fichiers inutilisés, ajoute du typage, gère les exceptions, sécurise la génération de mots de passe et améliore la gestion des logs.
This commit is contained in:
@@ -12,47 +12,54 @@ class SignClient
|
||||
{
|
||||
private \Docuseal\Api $docuseal;
|
||||
|
||||
public function __construct(private readonly RequestStack $requestStack,private readonly UploaderHelper $uploaderHelper,private readonly UrlGeneratorInterface $urlGenerator,private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->docuseal = new \Docuseal\Api($_ENV['DOCUSIGN_KEY'], $_ENV['DOCUSIGN_URL']);
|
||||
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly UploaderHelper $uploaderHelper,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
) {
|
||||
$key = $_ENV['DOCUSIGN_KEY'] ?? '';
|
||||
$url = $_ENV['DOCUSIGN_URL'] ?? '';
|
||||
$this->docuseal = new \Docuseal\Api($key, $url);
|
||||
}
|
||||
|
||||
public function createSubmissionDevis(CustomerDevis $devis)
|
||||
public function createSubmissionDevis(CustomerDevis $devis): string
|
||||
{
|
||||
if ($devis->getDevisSubmiterId() === null) {
|
||||
$currentRequest = $this->requestStack->getCurrentRequest();
|
||||
if ($currentRequest === null) {
|
||||
throw new \RuntimeException('No current request available');
|
||||
}
|
||||
|
||||
$t = new \DateTimeImmutable();
|
||||
|
||||
if($devis->getDevisSubmiterId() == null) {
|
||||
|
||||
$submissionId = $this->docuseal->createSubmissionFromPdf([
|
||||
'name' => 'Devis N°'.$devis->getNumDevis(),
|
||||
'completed_redirect_url' => $this->urlGenerator->generate('app_sign_complete',['type'=>'devis','id'=>$devis->getId()],UrlGeneratorInterface::ABSOLUTE_URL),
|
||||
'send_email' => false,
|
||||
'documents' => [
|
||||
[
|
||||
'name' => 'devis',
|
||||
'file' => $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost().$this->uploaderHelper->asset($devis,'devis'),
|
||||
]
|
||||
],
|
||||
'submitters' => [
|
||||
$submissionId = $this->docuseal->createSubmissionFromPdf([
|
||||
'name' => 'Devis N°' . $devis->getNumDevis(),
|
||||
'completed_redirect_url' => $this->urlGenerator->generate(
|
||||
'app_sign_complete',
|
||||
['type' => 'devis', 'id' => $devis->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
'send_email' => false,
|
||||
'documents' => [
|
||||
[
|
||||
'name' => 'devis',
|
||||
'file' => $currentRequest->getSchemeAndHttpHost() . $this->uploaderHelper->asset($devis, 'devis'),
|
||||
],
|
||||
],
|
||||
'submitters' => [
|
||||
[
|
||||
'role' => 'First Party',
|
||||
'email' => $devis->getCustomer()->mainContact()->getEmail(),
|
||||
]
|
||||
]
|
||||
]);
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$devis->setDevisSubmiterId($submissionId['id']);
|
||||
$this->entityManager->persist($devis);
|
||||
$this->entityManager->flush();
|
||||
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
|
||||
} else {
|
||||
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
|
||||
$devis->setDevisSubmiterId($submissionId['id']);
|
||||
$this->entityManager->persist($devis);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
return "https://signature.esy-web.dev/s/".$submissionData['slug'];
|
||||
$submissionData = $this->docuseal->getSubmitter($devis->getDevisSubmiterId());
|
||||
|
||||
return "https://signature.esy-web.dev/s/" . $submissionData['slug'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,72 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Generator;
|
||||
|
||||
/**
|
||||
* Class TempPasswordGenerator
|
||||
*
|
||||
* Provides functionality to generate secure temporary passwords.
|
||||
* Fournit des fonctionnalités pour générer des mots de passe temporaires sécurisés.
|
||||
*/
|
||||
class TempPasswordGenerator
|
||||
{
|
||||
private const DEFAULT_LENGTH = 12;
|
||||
private const DEFAULT_CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{}|;:,.<>?';
|
||||
|
||||
/**
|
||||
* Generates a random temporary password.
|
||||
* Génère un mot de passe temporaire aléatoire.
|
||||
*
|
||||
* @param int $length The desired length of the password. Default is 12 characters.
|
||||
* @param string $characters A string of characters to use for password generation.
|
||||
* Defaults to a mix of uppercase, lowercase, numbers, and symbols.
|
||||
* @return string The generated temporary password.
|
||||
* @param int $length Longueur désirée du mot de passe (par défaut 12).
|
||||
* @param string $characters Jeu de caractères à utiliser dans le mot de passe.
|
||||
* @return string Mot de passe généré.
|
||||
*/
|
||||
public static function generate(int $length = 12, string $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{}|;:,.<>?') : string
|
||||
public static function generate(int $length = self::DEFAULT_LENGTH, string $characters = self::DEFAULT_CHARACTERS): string
|
||||
{
|
||||
// Ensure the length is positive
|
||||
if ($length <= 0) {
|
||||
// You might want to throw an exception or return an empty string
|
||||
// depending on how you want to handle invalid lengths.
|
||||
// For simplicity, we'll default to 12 if an invalid length is provided.
|
||||
$length = 12;
|
||||
$length = self::DEFAULT_LENGTH;
|
||||
}
|
||||
|
||||
$password = '';
|
||||
$charactersLength = strlen($characters);
|
||||
$maxIndex = strlen($characters) - 1;
|
||||
|
||||
// Generate the password character by character
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
// Use random_int for cryptographically secure random number generation
|
||||
$password .= $characters[random_int(0, $charactersLength - 1)];
|
||||
$password .= $characters[random_int(0, $maxIndex)];
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a password meets certain complexity requirements (optional).
|
||||
* This is a basic example and can be extended.
|
||||
* Vérifie si un mot de passe remplit certaines exigences de complexité.
|
||||
*
|
||||
* @param string $password The password to check.
|
||||
* @return bool True if the password meets basic complexity, false otherwise.
|
||||
* @param string $password Mot de passe à vérifier.
|
||||
* @return bool True si les critères sont respectés, false sinon.
|
||||
*/
|
||||
public static function isComplex(string $password): bool
|
||||
{
|
||||
// Minimum length
|
||||
if (strlen($password) < 8) {
|
||||
return false;
|
||||
}
|
||||
// Requires at least one uppercase letter
|
||||
if (!preg_match('/[A-Z]/', $password)) {
|
||||
return false;
|
||||
}
|
||||
// Requires at least one lowercase letter
|
||||
if (!preg_match('/[a-z]/', $password)) {
|
||||
return false;
|
||||
}
|
||||
// Requires at least one number
|
||||
if (!preg_match('/[0-9]/', $password)) {
|
||||
return false;
|
||||
}
|
||||
// Requires at least one special character
|
||||
if (!preg_match('/[!@#$%^&*()-_=+\[\]{}|;:,.<>?]/', $password)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return strlen($password) >= 8
|
||||
&& preg_match('/[A-Z]/', $password)
|
||||
&& preg_match('/[a-z]/', $password)
|
||||
&& preg_match('/[0-9]/', $password)
|
||||
&& preg_match('/[!@#$%^&*()\-_=+\[\]{}|;:,.<>?]/', $password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Google\Cloud\Compute\V1\GetInstanceRequest;
|
||||
use Google\Cloud\Compute\V1\Instance;
|
||||
use Google\Cloud\Compute\V1\ListInstancesRequest;
|
||||
use Google\Cloud\Compute\V1\NetworkInterface;
|
||||
use Google\Protobuf\RepeatedField;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class ComputeEngineClient
|
||||
@@ -19,59 +18,83 @@ class ComputeEngineClient
|
||||
private string $projectId;
|
||||
private string $zone;
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,KernelInterface $kernel)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
KernelInterface $kernel
|
||||
) {
|
||||
$credentialsPath = $kernel->getProjectDir() . "/account.json";
|
||||
$this->client = new InstancesClient([
|
||||
'credentials' => $kernel->getProjectDir()."/account.json"
|
||||
'credentials' => $credentialsPath,
|
||||
]);
|
||||
$content = file_get_contents($kernel->getProjectDir()."/account.json");
|
||||
$content = json_decode($content);
|
||||
|
||||
$this->projectId = $content->project_id;
|
||||
$this->zone = "europe-west4-a";
|
||||
$content = json_decode(file_get_contents($credentialsPath), false);
|
||||
$this->projectId = $content->project_id ?? '';
|
||||
$this->zone = 'europe-west4-a';
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les instances compute commençant par 'srv-' en base ou en création.
|
||||
*
|
||||
* @return Compute[]
|
||||
*/
|
||||
public function list(): array
|
||||
{
|
||||
$request = (new ListInstancesRequest())
|
||||
$request = (new ListInstancesRequest())
|
||||
->setProject($this->projectId)
|
||||
->setZone($this->zone);
|
||||
|
||||
$instancesList = $this->client->list($request);
|
||||
|
||||
$instances = [];
|
||||
/** @var Instance $instance */
|
||||
foreach ($instancesList as $instance) {
|
||||
if(str_contains($instance->getName(),'srv-')) {
|
||||
if (str_contains($instance->getName(), 'srv-')) {
|
||||
/** @var NetworkInterface $network */
|
||||
$network = $instance->getNetworkInterfaces()[0];
|
||||
/** @var AccessConfig $accessConfig */
|
||||
$accessConfig = $network->getAccessConfigs()[0];
|
||||
|
||||
$compute = $this->entityManager->getRepository(Compute::class)->findOneBy(['instanceId'=>$instance->getId()]);
|
||||
if(!$compute instanceof Compute) {
|
||||
$compute = $this->entityManager->getRepository(Compute::class)
|
||||
->findOneBy(['instanceId' => $instance->getId()]);
|
||||
|
||||
if (!$compute instanceof Compute) {
|
||||
$compute = new Compute();
|
||||
$compute->setInstanceId($instance->getId());
|
||||
$compute->setZone(str_replace("https://www.googleapis.com/compute/v1/projects/".$this->projectId."/zones/","",$instance->getZone()));
|
||||
$compute->setZone(str_replace(
|
||||
"https://www.googleapis.com/compute/v1/projects/{$this->projectId}/zones/",
|
||||
'',
|
||||
$instance->getZone()
|
||||
));
|
||||
$compute->setInternalIp($network->getNetworkIP());
|
||||
$compute->setExternalIp($accessConfig->getNatIP());
|
||||
$compute->setStatus("down");
|
||||
$compute->setStatus('down');
|
||||
}
|
||||
|
||||
$this->entityManager->persist($compute);
|
||||
$instances[] = $compute;
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
public function detail(Compute $compute)
|
||||
/**
|
||||
* Charge les détails d’une instance Compute et met à jour le statut.
|
||||
*/
|
||||
public function detail(Compute $compute): Compute
|
||||
{
|
||||
$request = (new GetInstanceRequest())
|
||||
->setInstance($compute->getInstanceId())
|
||||
->setProject($this->projectId)
|
||||
->setZone($this->zone);
|
||||
|
||||
$instance = $this->client->get($request);
|
||||
|
||||
$compute->setStatus($instance->getStatus());
|
||||
$compute->name = $instance->getName();
|
||||
return$compute;
|
||||
|
||||
return $compute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,23 @@ use App\Service\Mailer\Mailer;
|
||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class LoggerEventListener
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(private Mailer $mailer,private readonly TokenStorageInterface $tokenStorage,LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
public function __construct(
|
||||
private readonly Mailer $mailer,
|
||||
private readonly TokenStorageInterface $tokenStorage,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public function postPersist(Logger $logger, LifecycleEventArgs $args): void
|
||||
{
|
||||
try {
|
||||
$logger->lock();
|
||||
} catch (ImmutableLoggerFieldException $e) {
|
||||
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postPersist) : ' . $e->getMessage());
|
||||
} catch (ImmutableLoggerFieldException $exception) {
|
||||
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postPersist) : ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +32,22 @@ class LoggerEventListener
|
||||
{
|
||||
try {
|
||||
$logger->lock();
|
||||
} catch (ImmutableLoggerFieldException $e) {
|
||||
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postLoad) : ' . $e->getMessage());
|
||||
} catch (ImmutableLoggerFieldException $exception) {
|
||||
$this->logger->warning('Tentative de verrouillage d\'une entité Logger déjà verrouillée (postLoad) : ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[AsEventListener(event: ImmutableLoggerFieldException::class)]
|
||||
public function onKernelException(ImmutableLoggerFieldException $event): void {
|
||||
$account = $this->tokenStorage->getToken();
|
||||
$account = $account->getUser();
|
||||
$this->mailer->sendMulti(["jovann@siteconseil.fr","legrand@siteconseil.fr"],"[Mainframe] - Tentative de modifier du log ! ","mails/artemis/error-logger.twig",[
|
||||
'account' => $account,
|
||||
]);
|
||||
public function onKernelException(ImmutableLoggerFieldException $exception): void
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
$account = $token ? $token->getUser() : null;
|
||||
|
||||
$this->mailer->sendMulti(
|
||||
['jovann@siteconseil.fr', 'legrand@siteconseil.fr'],
|
||||
'[Mainframe] - Tentative de modifier du log !',
|
||||
'mails/artemis/error-logger.twig',
|
||||
['account' => $account]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,34 +14,45 @@ class LoggerService
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly VaultClient $vaultClient,
|
||||
) {}
|
||||
private readonly VaultClient $vaultClient
|
||||
) {
|
||||
}
|
||||
|
||||
public function log(string $type, string $content, ?Account $account = null): Logger
|
||||
{
|
||||
|
||||
$entryAt = new \DateTimeImmutable();
|
||||
$uuid = Uuid::v4();
|
||||
|
||||
$logger = new Logger();
|
||||
$logger->setEvent($this->eventDispatcher);
|
||||
$logger->setEntryAt($entryAt)
|
||||
->setType($this->vaultClient->encrypt("mainframe_logger",$type))
|
||||
->setContent($this->vaultClient->encrypt("mainframe_logger",$content))
|
||||
->setType($this->vaultClient->encrypt('mainframe_logger', $type))
|
||||
->setContent($this->vaultClient->encrypt('mainframe_logger', $content))
|
||||
->setAccount($account)
|
||||
->setUuid($uuid);
|
||||
|
||||
$hmac = $this->generateHmac($logger);
|
||||
$logger->setHmac($this->vaultClient->encrypt("mainframe_logger",$hmac));
|
||||
$logger->setHmac($this->vaultClient->encrypt('mainframe_logger', $hmac));
|
||||
|
||||
$this->em->persist($logger);
|
||||
$this->em->flush();
|
||||
|
||||
return $logger;
|
||||
}
|
||||
|
||||
public function isLoggerTampered(Logger $logger): bool
|
||||
{
|
||||
$expectedHmac = $this->generateHmac($logger);
|
||||
return !hash_equals($expectedHmac, $logger->getHmac());
|
||||
$actualHmac = $logger->getHmac();
|
||||
|
||||
// Décrypter le HMAC stocké pour comparaison
|
||||
$decryptedHmac = $this->vaultClient->decrypt('mainframe_logger', (string) $actualHmac);
|
||||
|
||||
if ($decryptedHmac === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !hash_equals($expectedHmac, $decryptedHmac);
|
||||
}
|
||||
|
||||
private function generateHmac(Logger $logger): string
|
||||
@@ -49,17 +60,24 @@ class LoggerService
|
||||
$secretKey = $_ENV['APP_SECRET'] ?? 'change_this_secret';
|
||||
|
||||
$data = implode('|', [
|
||||
$logger->getType(),
|
||||
$logger->getContent(),
|
||||
$logger->getEntryAt()?->format(DATE_ATOM),
|
||||
$logger->getAccount()?->getId(),
|
||||
$logger->getUuid()?->toRfc4122(),
|
||||
$logger->getType() ?? '',
|
||||
$logger->getContent() ?? '',
|
||||
$logger->getEntryAt()?->format(DATE_ATOM) ?? '',
|
||||
$logger->getAccount()?->getId() ?? '',
|
||||
$logger->getUuid()?->toRfc4122() ?? '',
|
||||
]);
|
||||
|
||||
return hash_hmac('sha256', $data, $secretKey);
|
||||
}
|
||||
|
||||
public function load(Account $account)
|
||||
/**
|
||||
* Charge les logs pour un compte donné.
|
||||
* Typage ajouté.
|
||||
*
|
||||
* @param Account $account
|
||||
* @return Logger[]
|
||||
*/
|
||||
public function load(Account $account): array
|
||||
{
|
||||
return $this->em->getRepository(Logger::class)->load($account);
|
||||
}
|
||||
|
||||
@@ -3,48 +3,48 @@
|
||||
namespace App\Service\Mailer;
|
||||
|
||||
use App\Entity\Mail;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpKernel\Profiler\Profiler;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\Header\IdentificationHeader;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class AmazonSesClient
|
||||
{
|
||||
private Mailer $mailer;
|
||||
|
||||
public function __construct(
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$transport = new EsmtpTransport(
|
||||
'email-smtp.eu-west-3.amazonaws.com',
|
||||
587,
|
||||
false,
|
||||
$eventDispatcher
|
||||
);
|
||||
$transport->setUsername($_ENV['AMAZON_SES_PUBLIC'] ?? '');
|
||||
$transport->setPassword($_ENV['AMAZON_SES_SECRET'] ?? '');
|
||||
|
||||
private \Symfony\Component\Mailer\Mailer $mailer;
|
||||
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator,EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
//init ses client
|
||||
|
||||
$transport = new EsmtpTransport("email-smtp.eu-west-3.amazonaws.com",587,false,$eventDispatcher);
|
||||
$transport->setUsername($_ENV['AMAZON_SES_PUBLIC']);
|
||||
$transport->setPassword($_ENV['AMAZON_SES_SECRET']);
|
||||
|
||||
$this->mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
$this->mailer = new Mailer($transport);
|
||||
}
|
||||
|
||||
public function send(string $html,string $address,string $subject)
|
||||
/**
|
||||
* Envoie un email HTML avec tracking invisible.
|
||||
*/
|
||||
public function send(string $html, string $address, string $subject): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->from('no-reply@siteconseil.fr');
|
||||
$email->to($address);
|
||||
$email->subject($subject);
|
||||
|
||||
$email = (new Email())
|
||||
->from('no-reply@siteconseil.fr')
|
||||
->to($address)
|
||||
->subject($subject);
|
||||
|
||||
// Génération et ajout de l'en-tête Message-Id
|
||||
$messageId = $email->generateMessageId();
|
||||
$header = $email->getHeaders();
|
||||
$header->add(new IdentificationHeader("Message-Id",$messageId));
|
||||
$datas = $this->generateTracking($email);
|
||||
$email->getHeaders()->add(new IdentificationHeader('Message-Id', $messageId));
|
||||
|
||||
$tracking = $this->generateTracking($email);
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
@@ -52,35 +52,42 @@ class AmazonSesClient
|
||||
libxml_clear_errors();
|
||||
|
||||
$img = $dom->createElement('img');
|
||||
$img->setAttribute('src', $datas['url']);
|
||||
$img->setAttribute('width',0);
|
||||
$img->setAttribute('height',0);
|
||||
$img->setAttribute('src', $tracking['url']);
|
||||
$img->setAttribute('width', '0');
|
||||
$img->setAttribute('height', '0');
|
||||
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
if ($body) {
|
||||
// Ajouter <img> avant la fin de </body>
|
||||
$body->appendChild($img);
|
||||
}
|
||||
$newHtml = $dom->saveHTML();
|
||||
$email->html($newHtml);
|
||||
$this->mailer->send($email);
|
||||
|
||||
$email->html($newHtml);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
|
||||
private function generateTracking(Email $email)
|
||||
/**
|
||||
* Génère les données de tracking et l’URL associée.
|
||||
*
|
||||
* @param Email $email
|
||||
* @return array{object: Mail, url: string}
|
||||
*/
|
||||
private function generateTracking(Email $email): array
|
||||
{
|
||||
$messageFormat = $email->getHeaders()->get('message-id')->getBody()[0];
|
||||
$messageFormat = str_replace("@siteconseil.fr","",$messageFormat);
|
||||
$messageIdHeader = $email->getHeaders()->get('message-id');
|
||||
$messageFormat = $messageIdHeader ? $messageIdHeader->getBody()[0] : '';
|
||||
$messageFormat = str_replace('@siteconseil.fr', '', $messageFormat);
|
||||
|
||||
$mailData = new Mail();
|
||||
$mailData->setDest($email->getTo()[0]->getAddress());
|
||||
$mailData->setSubject($email->getSubject());
|
||||
$mailData->setMessageId($messageFormat);
|
||||
$mailData->setStatus("draft");
|
||||
$mailData->setStatus('draft');
|
||||
|
||||
return [
|
||||
'object' => $mailData,
|
||||
'url'=> "https://mainframe.esy-web.dev".$this->urlGenerator->generate('app_tracking',['slug'=>$messageFormat])
|
||||
'url' => 'https://mainframe.esy-web.dev' . $this->urlGenerator->generate('app_tracking', ['slug' => $messageFormat]),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Mailer\Event;
|
||||
|
||||
use App\Entity\Account;
|
||||
|
||||
class CreatedAdminEvent
|
||||
{
|
||||
private Account $account;
|
||||
private string $password;
|
||||
|
||||
public function __construct(Account $account, string $password)
|
||||
{
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
public function __construct(
|
||||
private readonly Account $account,
|
||||
private readonly string $password
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account
|
||||
*/
|
||||
public function getAccount(): Account
|
||||
{
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
|
||||
@@ -19,163 +19,199 @@ use Twig\Environment;
|
||||
|
||||
class Mailer
|
||||
{
|
||||
private readonly MailerInterface $mailer;
|
||||
|
||||
private \Symfony\Component\Mailer\Mailer $mailer;
|
||||
|
||||
public function __construct(MailerInterface $mailer,private readonly EntityManagerInterface $entityManager,private readonly UrlGeneratorInterface $urlGenerator,private readonly ?Profiler $profiler,private readonly Environment $environment)
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
MailerInterface $mailer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly ?Profiler $profiler,
|
||||
private readonly Environment $environment,
|
||||
) {
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
private function convertMjmlToHtml(string $mjmlContent): string
|
||||
{
|
||||
$command = ['mjml', '--stdin'];
|
||||
$process = new Process($command);
|
||||
$process = new Process(['mjml', '--stdin']);
|
||||
$process->setInput($mjmlContent);
|
||||
|
||||
try {
|
||||
$process->setInput($mjmlContent);
|
||||
$process->run();
|
||||
|
||||
// Exécute la commande et vérifie la réussite
|
||||
if (!$process->isSuccessful()) {
|
||||
throw new ProcessFailedException($process);
|
||||
}
|
||||
|
||||
return $process->getOutput();
|
||||
} catch (ProcessFailedException $exception) {
|
||||
return ''; // Retourne une chaîne vide en cas d'échec
|
||||
} catch (Exception $e) {
|
||||
return '';
|
||||
} catch (ProcessFailedException|Exception) {
|
||||
return ''; // Retourne vide en cas d'échec
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTest()
|
||||
public function sendTest(): void
|
||||
{
|
||||
$dest = new Address("test-dev@siteconseil.fr", "Test Dev");
|
||||
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
|
||||
$mail = new Email();
|
||||
$mail->subject("Test de configuration");
|
||||
$mail->to($dest);
|
||||
$mail->from($src);
|
||||
|
||||
$mail = (new Email())
|
||||
->subject("Test de configuration")
|
||||
->to($dest)
|
||||
->from($src);
|
||||
|
||||
$messageId = $mail->generateMessageId();
|
||||
$header = $mail->getHeaders();
|
||||
$header->add(new IdentificationHeader("Message-Id",$messageId));
|
||||
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
|
||||
$datas = $this->generateTracking($mail);
|
||||
/** @var Mail $object */
|
||||
$object= $datas['object'];
|
||||
$mjmlGenerator = $this->environment->render('mails/test.twig',[
|
||||
$object = $datas['object'];
|
||||
|
||||
$mjmlGenerator = $this->environment->render('mails/test.twig', [
|
||||
'system' => [
|
||||
'subject' => 'Test de configuration',
|
||||
'tracking_url'=>$datas['url'],
|
||||
]
|
||||
'tracking_url' => $datas['url'],
|
||||
],
|
||||
]);
|
||||
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
|
||||
|
||||
$object->setContent($htmlContent);
|
||||
|
||||
$mail->html($htmlContent);
|
||||
|
||||
try {
|
||||
$this->mailer->send($mail);
|
||||
$object->setStatus("sent");
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
$object->setStatus("error");
|
||||
}
|
||||
|
||||
$this->entityManager->persist($object);
|
||||
$this->entityManager->flush();
|
||||
|
||||
}
|
||||
|
||||
public function send(string $address, string $addressName, string $subject, string $template, array $data,array $files = [])
|
||||
{
|
||||
/**
|
||||
* @param string $address
|
||||
* @param string $addressName
|
||||
* @param string $subject
|
||||
* @param string $template
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<\Symfony\Component\Mime\Part\Part> $files
|
||||
*/
|
||||
public function send(
|
||||
string $address,
|
||||
string $addressName,
|
||||
string $subject,
|
||||
string $template,
|
||||
array $data,
|
||||
array $files = []
|
||||
): void {
|
||||
$dest = new Address($address, $addressName);
|
||||
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
|
||||
$mail = new Email();
|
||||
$mail->subject($subject);
|
||||
$mail->to($dest);
|
||||
$mail->from($src);
|
||||
|
||||
$mail = (new Email())
|
||||
->subject($subject)
|
||||
->to($dest)
|
||||
->from($src);
|
||||
|
||||
$messageId = $mail->generateMessageId();
|
||||
$header = $mail->getHeaders();
|
||||
$header->add(new IdentificationHeader("Message-Id",$messageId));
|
||||
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
|
||||
$datasSign = $this->generateTracking($mail);
|
||||
/** @var Mail $object */
|
||||
$object = $datasSign['object'];
|
||||
|
||||
$mjmlGenerator = $this->environment->render($template, [
|
||||
'system' => [
|
||||
'subject' => $subject,
|
||||
'tracking_url'=>$datasSign['url']
|
||||
'tracking_url' => $datasSign['url'],
|
||||
],
|
||||
'datas' => $data,
|
||||
]);
|
||||
|
||||
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
|
||||
$object->setContent($htmlContent);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$mail->addPart($file);
|
||||
$mail->addPart($file);
|
||||
}
|
||||
|
||||
$mail->html($htmlContent);
|
||||
|
||||
try {
|
||||
$this->mailer->send($mail);
|
||||
$object->setStatus("sent");
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
$object->setStatus("error");
|
||||
}
|
||||
|
||||
$this->entityManager->persist($object);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function generateTracking(Email $email)
|
||||
/**
|
||||
* @param Email $email
|
||||
* @return array{object: Mail, url: string}
|
||||
*/
|
||||
private function generateTracking(Email $email): array
|
||||
{
|
||||
$messageFormat = $email->getHeaders()->get('message-id')->getBody()[0];
|
||||
$messageFormat = str_replace("@esy-web.dev","",$messageFormat);
|
||||
$messageIdHeader = $email->getHeaders()->get('message-id');
|
||||
$messageFormat = $messageIdHeader ? $messageIdHeader->getBody()[0] : '';
|
||||
$messageFormat = str_replace("@esy-web.dev", "", $messageFormat);
|
||||
|
||||
$mailData = new Mail();
|
||||
$mailData->setDest($email->getTo()[0]->getAddress());
|
||||
$mailData->setSubject($email->getSubject());
|
||||
$mailData->setMessageId($messageFormat);
|
||||
$mailData->setStatus("draft");
|
||||
return [
|
||||
'object' => $mailData,
|
||||
'url'=> "https://mainframe.esy-web.dev".$this->urlGenerator->generate('app_tracking',['slug'=>$messageFormat])
|
||||
];
|
||||
|
||||
return [
|
||||
'object' => $mailData,
|
||||
'url' => "https://mainframe.esy-web.dev" . $this->urlGenerator->generate('app_tracking', ['slug' => $messageFormat]),
|
||||
];
|
||||
}
|
||||
|
||||
public function sendMulti(array $addressList, string $subject, string $template, array $data)
|
||||
/**
|
||||
* @param string[] $addressList
|
||||
* @param string $subject
|
||||
* @param string $template
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function sendMulti(array $addressList, string $subject, string $template, array $data): void
|
||||
{
|
||||
|
||||
$src = new Address("mainframe@esy-web.dev", "Mainframe EsyWeb");
|
||||
$mail = new Email();
|
||||
$mail->subject($subject);
|
||||
$mail = (new Email())->subject($subject);
|
||||
|
||||
foreach ($addressList as $address) {
|
||||
$dest = new Address($address);
|
||||
$mail->addTo($dest);
|
||||
}
|
||||
|
||||
$mail->from($src);
|
||||
|
||||
$messageId = $mail->generateMessageId();
|
||||
$header = $mail->getHeaders();
|
||||
$header->add(new IdentificationHeader("Message-Id",$messageId));
|
||||
$mail->getHeaders()->add(new IdentificationHeader("Message-Id", $messageId));
|
||||
|
||||
$datasSign = $this->generateTracking($mail);
|
||||
/** @var Mail $object */
|
||||
$object = $datasSign['object'];
|
||||
|
||||
$mjmlGenerator = $this->environment->render($template, [
|
||||
'system' => [
|
||||
'subject' => $subject,
|
||||
'tracking_url'=>$datasSign['url']
|
||||
'tracking_url' => $datasSign['url'],
|
||||
],
|
||||
'datas' => $data,
|
||||
]);
|
||||
|
||||
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
|
||||
$object->setContent($htmlContent);
|
||||
|
||||
$mail->html($htmlContent);
|
||||
|
||||
try {
|
||||
$this->mailer->send($mail);
|
||||
$object->setStatus("sent");
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
$object->setStatus("error");
|
||||
}
|
||||
|
||||
$this->entityManager->persist($object);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Mailer;
|
||||
|
||||
use App\Service\Mailer\Event\CreatedAdminEvent;
|
||||
@@ -8,19 +9,27 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
#[AsEventListener(event: CreatedAdminEvent::class, method: 'onAdminEvent')]
|
||||
class MailerSubscriber
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly Mailer $mailer)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly Mailer $mailer
|
||||
) {
|
||||
}
|
||||
|
||||
public function onAdminEvent(CreatedAdminEvent $createdAdminEvent)
|
||||
public function onAdminEvent(CreatedAdminEvent $createdAdminEvent): void
|
||||
{
|
||||
$account = $createdAdminEvent->getAccount();
|
||||
$password = $createdAdminEvent->getPassword();
|
||||
|
||||
$this->mailer->send($account->getEmail(), $account->getUsername(), "[MainFrame] - Création d'un compte administrateur", "mails/artemis/new_admin.twig", [
|
||||
'username' => $account->getUsername(),
|
||||
'password' => $password,
|
||||
'url' => $this->urlGenerator->generate('app_login',[],UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
]);
|
||||
$this->mailer->send(
|
||||
$account->getEmail(),
|
||||
$account->getUsername(),
|
||||
"[MainFrame] - Création d'un compte administrateur",
|
||||
"mails/artemis/new_admin.twig",
|
||||
[
|
||||
'username' => $account->getUsername(),
|
||||
'password' => $password,
|
||||
'url' => $this->urlGenerator->generate('app_login', [], UrlGeneratorInterface::ABSOLUTE_URL),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,25 +11,37 @@ class Client
|
||||
public function __construct()
|
||||
{
|
||||
$this->ovhClient = new Api(
|
||||
$_ENV['OVH_KEY'],
|
||||
$_ENV['OVH_SECRET'],
|
||||
"ovh-eu",
|
||||
$_ENV['OVH_CUSTOMER']
|
||||
$_ENV['OVH_KEY'] ?? '',
|
||||
$_ENV['OVH_SECRET'] ?? '',
|
||||
'ovh-eu',
|
||||
$_ENV['OVH_CUSTOMER'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les informations du domaine OVH.
|
||||
*
|
||||
* @param string|null $ndd Nom de domaine
|
||||
* @return array|null Tableau avec date d'expiration et serveurs DNS ou null en cas d'erreur / domaine vide
|
||||
*/
|
||||
public function info(?string $ndd): ?array
|
||||
{
|
||||
if (empty($ndd)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$ndd = $this->ovhClient->get('/domain/' . $ndd);
|
||||
$domainInfo = $this->ovhClient->get('/domain/' . $ndd);
|
||||
|
||||
return [
|
||||
'expired' => $ndd['expirationDate'],
|
||||
'expired' => $domainInfo['expirationDate'] ?? null,
|
||||
'nameServer' => [
|
||||
0 => $ndd['nameServers'][0]['nameServer'],
|
||||
1 => $ndd['nameServers'][1]['nameServer'],
|
||||
]
|
||||
$domainInfo['nameServers'][0]['nameServer'] ?? null,
|
||||
$domainInfo['nameServers'][1]['nameServer'] ?? null,
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// Loger l'erreur si besoin ou gérer autrement
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,148 +11,145 @@ define('EURO', chr(128));
|
||||
|
||||
class DevisPdf extends FPDF
|
||||
{
|
||||
private $items;
|
||||
private array $items;
|
||||
|
||||
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerDevis $customerDevis)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly CustomerDevis $customerDevis
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$items = [];
|
||||
foreach ($this->customerDevis->getCustomerDevisLines() as $line) {
|
||||
$items[$line->getPos()] = [
|
||||
'title' => $line->getName(),
|
||||
'content' => $line->getContent(),
|
||||
'priceHt' => $line->getPriceHT(),
|
||||
'priceTTC' => (1.20 * $line->getPriceHT()),
|
||||
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
|
||||
];
|
||||
}
|
||||
ksort($items);
|
||||
$this->items = $items;
|
||||
$this->SetTitle(mb_convert_encoding("Devis N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumDevis());
|
||||
|
||||
$title = mb_convert_encoding("Devis N° " . $this->customerDevis->getNumDevis(), "ISO-8859-1", "UTF-8");
|
||||
$this->SetTitle($title);
|
||||
}
|
||||
|
||||
function Header()
|
||||
public function Header(): void
|
||||
{
|
||||
$this->SetFont('Arial', '', 10);
|
||||
|
||||
$formatter = new IntlDateFormatter(
|
||||
'fr_FR', // Locale for French (France)
|
||||
IntlDateFormatter::FULL, // Date style: e.g., jeudi 31 juillet 2025
|
||||
IntlDateFormatter::NONE, // Time style: none
|
||||
'Europe/Paris', // Timezone (important for correct date if not UTC)
|
||||
IntlDateFormatter::GREGORIAN, // Calendar type
|
||||
'fr_FR',
|
||||
IntlDateFormatter::FULL,
|
||||
IntlDateFormatter::NONE,
|
||||
'Europe/Paris',
|
||||
IntlDateFormatter::GREGORIAN
|
||||
);
|
||||
$this->Text(15, 80, mb_convert_encoding("DEVIS N° " . $this->customerDevis->getNumDevis(), 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(15, 85, mb_convert_encoding("Saint-Quentin, ".$formatter->format( $this->customerDevis->getCreateAt()->getTimestamp()), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$numDevisText = mb_convert_encoding("DEVIS N° " . $this->customerDevis->getNumDevis(), 'ISO-8859-1', 'UTF-8');
|
||||
$dateText = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerDevis->getCreateAt()), 'ISO-8859-1', 'UTF-8');
|
||||
|
||||
$this->Text(15, 80, $numDevisText);
|
||||
$this->Text(15, 85, $dateText);
|
||||
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
|
||||
$y = 60;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
|
||||
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
|
||||
$this->body();
|
||||
$customer = $this->customerDevis->getCustomer();
|
||||
|
||||
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
if ($address2 = $customer->getAddress2()) {
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
|
||||
if ($address3 = $customer->getAddress3()) {
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
|
||||
$y += 5;
|
||||
$cityLine = $customer->getZipcode() . " " . $customer->getCity();
|
||||
$this->Text(110, $y, mb_convert_encoding($cityLine, 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$this->body();
|
||||
}
|
||||
|
||||
public function body()
|
||||
private function body(): void
|
||||
{
|
||||
// Headers for the items table
|
||||
$this->SetFont('Arial','B',10);
|
||||
$this->SetXY(145,100);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->SetXY(145, 100);
|
||||
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'C');
|
||||
|
||||
$this->Line(145, 110, 145, 220);
|
||||
$this->Line(185, 110, 185, 220);
|
||||
|
||||
$this->Line(0,100,5,100);
|
||||
$this->Line(0,200,5,200);
|
||||
$this->Line(0, 100, 5, 100);
|
||||
$this->Line(0, 200, 5, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the headers for the items table.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates the main content of the PDF, including the list of items.
|
||||
* This function has been fixed to correctly display items one after another.
|
||||
*/
|
||||
function generate()
|
||||
public function generate(): void
|
||||
{
|
||||
$this->AliasNbPages();
|
||||
$this->AddPage();
|
||||
$this->SetFont('Arial', '', 12);
|
||||
|
||||
// Set a starting Y position for the items list, just below the item table header.
|
||||
$startY = 110;
|
||||
$this->SetY($startY);
|
||||
|
||||
// Define a bottom limit for the content to avoid overwriting the summary/footer.
|
||||
$contentBottomLimit = 220;
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
// A simple check to see if we have enough space for the next item.
|
||||
// 30 is a rough estimate for item height. For more complex content,
|
||||
// you might need a more dynamic height calculation.
|
||||
if ($this->GetY() + 30 > $contentBottomLimit) {
|
||||
$this->AddPage();
|
||||
$this->body(); // Redraw the item table header on the new page
|
||||
$this->SetY($startY); // Reset Y position on the new page
|
||||
$this->body(); // redraw table headers
|
||||
$this->SetY($startY);
|
||||
}
|
||||
|
||||
// Store the current Y position to align all columns for this item's title line.
|
||||
$current_y = $this->GetY();
|
||||
$currentY = $this->GetY();
|
||||
|
||||
// Set position to the start of the line for the main content.
|
||||
// Title
|
||||
$this->SetX(20);
|
||||
$this->SetFont('Arial', 'B', 11);
|
||||
// Print the title, but don't move to the next line yet (ln=0).
|
||||
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
|
||||
|
||||
// Now, set the position for the prices on the same line using SetXY.
|
||||
// Prices
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->SetXY(142, $current_y);
|
||||
$this->SetXY(142, $currentY);
|
||||
$this->SetFont('Arial', 'B', 11);
|
||||
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO, 0, 1, 'R');
|
||||
$this->SetFont('Arial', '', 11);
|
||||
// The cursor is now on the line below the title/prices.
|
||||
// Print the item content description.
|
||||
$this->SetX(30); // Ensure we are in the correct column.
|
||||
|
||||
// Content description
|
||||
$this->SetX(30);
|
||||
$this->MultiCell(90, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
|
||||
|
||||
// Add a small vertical gap between items for readability.
|
||||
$this->Ln(5);
|
||||
}
|
||||
|
||||
$this->displaySummary();
|
||||
}
|
||||
|
||||
function displaySummary()
|
||||
private function displaySummary(): void
|
||||
{
|
||||
|
||||
// Calculate totals
|
||||
$totalHT = array_sum(array_column($this->items, 'priceHt'));
|
||||
$totalTVA = $totalHT * 0.20;
|
||||
$totalTTC = $totalHT + $totalTVA;
|
||||
|
||||
// Position the summary block at the bottom of the page
|
||||
$this->SetY(-60);
|
||||
|
||||
$this->Cell(30,10,"{{Sign;type=signature;role=First Party}}", 0, 0, 'L');
|
||||
// Display the summary
|
||||
$this->Cell(30, 10, "{{Sign;type=signature;role=First Party}}", 0, 0, 'L');
|
||||
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->Cell(100, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
|
||||
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO, 0, 1, 'R');
|
||||
|
||||
$this->Cell(135, 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->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(135, 10, mb_convert_encoding('Total :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
|
||||
$this->Cell(35, 10, number_format($totalTTC, 2, ",") . " " . EURO, 0, 1, 'R');
|
||||
|
||||
@@ -2,161 +2,154 @@
|
||||
|
||||
namespace App\Service\Pdf;
|
||||
|
||||
use App\Entity\CustomerAdvertPayment;
|
||||
use App\Entity\CustomerOrder;
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\Label\Font\OpenSans;
|
||||
use Endroid\QrCode\Label\LabelAlignment;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Fpdf\Fpdf;
|
||||
use IntlDateFormatter;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
define('EURO_FACTURE', chr(128));
|
||||
|
||||
class FacturePdf extends Fpdf
|
||||
{
|
||||
private $items;
|
||||
private array $items;
|
||||
|
||||
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerOrder $customerDevis)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly CustomerOrder $customerDevis
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$items = [];
|
||||
foreach ($this->customerDevis->getCustomerOrderLines() as $line) {
|
||||
$items[$line->getPo()] = [
|
||||
'title' => $line->getName(),
|
||||
'content' => $line->getContent(),
|
||||
'priceHt' => $line->getPriceHT(),
|
||||
'priceTTC' => (1.20 * $line->getPriceHT()),
|
||||
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
|
||||
];
|
||||
}
|
||||
ksort($items);
|
||||
$this->items = $items;
|
||||
$this->SetTitle(mb_convert_encoding("Facture N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumOrder());
|
||||
|
||||
$title = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), "ISO-8859-1", "UTF-8");
|
||||
$this->SetTitle($title);
|
||||
}
|
||||
|
||||
function Header()
|
||||
public function Header(): void
|
||||
{
|
||||
$this->SetFont('Arial', '', 10);
|
||||
|
||||
$formatter = new IntlDateFormatter(
|
||||
'fr_FR', // Locale for French (France)
|
||||
IntlDateFormatter::FULL, // Date style: e.g., jeudi 31 juillet 2025
|
||||
IntlDateFormatter::NONE, // Time style: none
|
||||
'Europe/Paris', // Timezone (important for correct date if not UTC)
|
||||
IntlDateFormatter::GREGORIAN, // Calendar type
|
||||
'fr_FR',
|
||||
IntlDateFormatter::FULL,
|
||||
IntlDateFormatter::NONE,
|
||||
'Europe/Paris',
|
||||
IntlDateFormatter::GREGORIAN
|
||||
);
|
||||
|
||||
$factureNumber = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8');
|
||||
$dateFacture = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerDevis->getCreateAt()), 'ISO-8859-1', 'UTF-8');
|
||||
|
||||
$this->Text(15, 80, mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(15, 85, mb_convert_encoding("Saint-Quentin, ".$formatter->format( $this->customerDevis->getCreateAt()->getTimestamp()), 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(15, 80, $factureNumber);
|
||||
$this->Text(15, 85, $dateFacture);
|
||||
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$y = 60;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
|
||||
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
|
||||
$customer = $this->customerDevis->getCustomer();
|
||||
|
||||
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
if ($address2 = $customer->getAddress2()) {
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
if ($address3 = $customer->getAddress3()) {
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
|
||||
}
|
||||
$y = $y + 5;
|
||||
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$y += 5;
|
||||
$this->Text(110, $y, mb_convert_encoding(
|
||||
$customer->getZipcode() . " " . $customer->getCity(),
|
||||
'ISO-8859-1',
|
||||
'UTF-8'
|
||||
));
|
||||
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->body();
|
||||
}
|
||||
|
||||
public function body()
|
||||
private function body(): void
|
||||
{
|
||||
// Headers for the items table
|
||||
$this->SetFont('Arial','B',10);
|
||||
$this->SetXY(145,100);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->SetXY(145, 100);
|
||||
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'C');
|
||||
|
||||
$this->Line(145, 110, 145, 220);
|
||||
$this->Line(185, 110, 185, 220);
|
||||
|
||||
$this->Line(0,100,5,100);
|
||||
$this->Line(0,200,5,200);
|
||||
$this->Line(0, 100, 5, 100);
|
||||
$this->Line(0, 200, 5, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the main content of the PDF, including the list of items.
|
||||
* This function has been fixed to correctly display items one after another.
|
||||
*/
|
||||
function generate()
|
||||
public function generate(): void
|
||||
{
|
||||
$this->AliasNbPages();
|
||||
$this->AddPage();
|
||||
$this->SetFont('Arial', '', 12);
|
||||
|
||||
// Set a starting Y position for the items list, just below the item table header.
|
||||
$startY = 110;
|
||||
$this->SetY($startY);
|
||||
|
||||
// Define a bottom limit for the content to avoid overwriting the summary/footer.
|
||||
$contentBottomLimit = 220;
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
// A simple check to see if we have enough space for the next item.
|
||||
// 30 is a rough estimate for item height. For more complex content,
|
||||
// you might need a more dynamic height calculation.
|
||||
if ($this->GetY() + 30 > $contentBottomLimit) {
|
||||
$this->AddPage();
|
||||
$this->body(); // Redraw the item table header on the new page
|
||||
$this->SetY($startY); // Reset Y position on the new page
|
||||
$this->body(); // redraw headers
|
||||
$this->SetY($startY);
|
||||
}
|
||||
|
||||
// Store the current Y position to align all columns for this item's title line.
|
||||
$current_y = $this->GetY();
|
||||
$currentY = $this->GetY();
|
||||
|
||||
// Set position to the start of the line for the main content.
|
||||
// Title
|
||||
$this->SetX(20);
|
||||
$this->SetFont('Arial', 'B', 11);
|
||||
// Print the title, but don't move to the next line yet (ln=0).
|
||||
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
|
||||
|
||||
// Now, set the position for the prices on the same line using SetXY.
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->SetXY(142, $current_y);
|
||||
// Price HT
|
||||
$this->SetFont('Arial', 'B', 11);
|
||||
$this->SetXY(142, $currentY);
|
||||
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
|
||||
$this->SetFont('Arial', '', 11);
|
||||
// The cursor is now on the line below the title/prices.
|
||||
// Print the item content description.
|
||||
$this->SetX(30); // Ensure we are in the correct column.
|
||||
|
||||
// Content description
|
||||
$this->SetX(30);
|
||||
$this->MultiCell(90, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
|
||||
|
||||
// Add a small vertical gap between items for readability.
|
||||
$this->Ln(5);
|
||||
}
|
||||
|
||||
$this->displaySummary();
|
||||
}
|
||||
|
||||
|
||||
function displaySummary()
|
||||
private function displaySummary(): void
|
||||
{
|
||||
|
||||
// Calculate totals
|
||||
$totalHT = array_sum(array_column($this->items, 'priceHt'));
|
||||
$totalTVA = $totalHT * 0.20;
|
||||
$totalTTC = $totalHT + $totalTVA;
|
||||
|
||||
// Position the summary block at the bottom of the page
|
||||
$this->SetY(-60);
|
||||
|
||||
// Display the summary
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->Cell(135, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
|
||||
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
|
||||
|
||||
$this->Cell(135, 10, mb_convert_encoding('TVA (20%) :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
|
||||
$this->Cell(40, 10, number_format($totalTVA, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
|
||||
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(135, 10, mb_convert_encoding('Total :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
|
||||
$this->Cell(40, 10, number_format($totalTTC, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
|
||||
|
||||
@@ -16,30 +16,39 @@ define('EURO_AVIS', chr(128));
|
||||
|
||||
class PaymentPdf extends Fpdf
|
||||
{
|
||||
private $items;
|
||||
private array $items;
|
||||
|
||||
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerAdvertPayment $customerDevis, private readonly string $urlPaiment)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly CustomerAdvertPayment $customerDevis,
|
||||
private readonly string $urlPaiment
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$items = [];
|
||||
foreach ($this->customerDevis->getCustomerAdvertPaymentLines() as $line) {
|
||||
$items[$line->getPos()] = [
|
||||
'title' => $line->getName(),
|
||||
'content' => $line->getContent(),
|
||||
'priceHt' => $line->getPriceHT(),
|
||||
'priceTTC' => (1.20 * $line->getPriceHT()),
|
||||
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
|
||||
];
|
||||
}
|
||||
|
||||
ksort($items);
|
||||
$this->items = $items;
|
||||
$this->SetTitle(mb_convert_encoding("Avis de paiment N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumAvis());
|
||||
|
||||
$title = mb_convert_encoding("Avis de paiement N° " . $this->customerDevis->getNumAvis(), "ISO-8859-1", "UTF-8");
|
||||
$this->SetTitle($title);
|
||||
}
|
||||
|
||||
function Header()
|
||||
public function Header(): void
|
||||
{
|
||||
$this->Image($this->kernel->getProjectDir() . "/public/assets/logo_siteconseil.png", 5, 5, 25);
|
||||
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Text(30, 10, mb_convert_encoding("SITECONSEIL", 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->Text(30, 15, mb_convert_encoding("27 rue le sérurier", 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(30, 20, mb_convert_encoding("02100 SAINT-QUENTIN", 'ISO-8859-1', 'UTF-8'));
|
||||
@@ -48,32 +57,39 @@ class PaymentPdf extends Fpdf
|
||||
$this->Text(8, 35, mb_convert_encoding("SIRET: 41866405800025", 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(8, 40, mb_convert_encoding("RCS: RCS St-Quentin 418 664 058", 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(8, 45, mb_convert_encoding("TVA: FR05418664058", 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Text(125, 10, mb_convert_encoding("AVIS DE PAIEMENT N° " . $this->customerDevis->getNumAvis(), 'ISO-8859-1', 'UTF-8'));
|
||||
$this->Text(125, 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'));
|
||||
$customer = $this->customerDevis->getCustomer();
|
||||
|
||||
$this->Text(120, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
|
||||
$y += 5;
|
||||
$this->Text(120, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
if ($address2 = $customer->getAddress2()) {
|
||||
$y += 5;
|
||||
$this->Text(120, $y, mb_convert_encoding($address2, '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'));
|
||||
|
||||
if ($address3 = $customer->getAddress3()) {
|
||||
$y += 5;
|
||||
$this->Text(120, $y, mb_convert_encoding($address3, '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'));
|
||||
|
||||
$y += 5;
|
||||
$this->Text(120, $y, mb_convert_encoding($customer->getZipcode() . " " . $customer->getCity(), 'ISO-8859-1', 'UTF-8'));
|
||||
|
||||
$this->body();
|
||||
}
|
||||
|
||||
function Footer()
|
||||
public function Footer(): void
|
||||
{
|
||||
$this->SetY(-20);
|
||||
$this->SetFont('Arial', 'I', 8);
|
||||
|
||||
$this->Cell(0, 5, 'SITECONSEIL - 27 rue le serrurier - 02100 SAINT-QUENTIN - s.com@siteconseil.fr', 0, 0, 'C');
|
||||
$this->Ln(5);
|
||||
$this->Cell(0, 5, '03 23 05 62 43 - SIRET: 41866405800025 - RCS: RCS St-Quentin - TVA: FR05418664058', 0, 0, 'C');
|
||||
@@ -81,10 +97,9 @@ class PaymentPdf extends Fpdf
|
||||
$this->Cell(0, 5, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
|
||||
}
|
||||
|
||||
public function body()
|
||||
private function body(): void
|
||||
{
|
||||
// Headers for the items table
|
||||
$this->SetFont('Arial','B',10);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->SetY(65);
|
||||
$this->SetX(120);
|
||||
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'R');
|
||||
@@ -92,65 +107,49 @@ class PaymentPdf extends Fpdf
|
||||
$this->Line(10, 70, 200, 70);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the main content of the PDF, including the list of items.
|
||||
* This function has been fixed to correctly display items one after another.
|
||||
*/
|
||||
function generate()
|
||||
public function generate(): void
|
||||
{
|
||||
$this->AliasNbPages();
|
||||
$this->AddPage();
|
||||
$this->SetFont('Arial', '', 12);
|
||||
|
||||
// Set a starting Y position for the items list, just below the item table header.
|
||||
$startY = 75;
|
||||
$this->SetY($startY);
|
||||
|
||||
// Define a bottom limit for the content to avoid overwriting the summary/footer.
|
||||
$contentBottomLimit = 220;
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
// A simple check to see if we have enough space for the next item.
|
||||
// 30 is a rough estimate for item height. For more complex content,
|
||||
// you might need a more dynamic height calculation.
|
||||
if ($this->GetY() + 30 > $contentBottomLimit) {
|
||||
$this->AddPage();
|
||||
$this->body(); // Redraw the item table header on the new page
|
||||
$this->SetY($startY); // Reset Y position on the new page
|
||||
$this->body();
|
||||
$this->SetY($startY);
|
||||
}
|
||||
|
||||
// Store the current Y position to align all columns for this item's title line.
|
||||
$current_y = $this->GetY();
|
||||
$currentY = $this->GetY();
|
||||
|
||||
// Set position to the start of the line for the main content.
|
||||
// Title
|
||||
$this->SetX(10);
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
// Print the title, but don't move to the next line yet (ln=0).
|
||||
$this->Cell(110, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
|
||||
|
||||
// Now, set the position for the prices on the same line using SetXY.
|
||||
// Prices HT and TTC
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->SetXY(120, $current_y);
|
||||
$this->SetXY(120, $currentY);
|
||||
$this->Cell(40, 10, number_format($item['priceHt'], 2, ",") . " " . EURO_AVIS, 0, 0, 'R');
|
||||
|
||||
$this->SetXY(160, $current_y);
|
||||
// The last cell on the line moves the cursor to the next line (ln=1).
|
||||
$this->SetXY(160, $currentY);
|
||||
$this->Cell(40, 10, number_format($item['priceTTC'], 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
|
||||
|
||||
// The cursor is now on the line below the title/prices.
|
||||
// Print the item content description.
|
||||
$this->SetX(10); // Ensure we are in the correct column.
|
||||
// Content description
|
||||
$this->SetX(10);
|
||||
$this->MultiCell(110, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
|
||||
|
||||
// Add a small vertical gap between items for readability.
|
||||
$this->Ln(5);
|
||||
}
|
||||
|
||||
$this->displaySummary();
|
||||
}
|
||||
|
||||
|
||||
function displaySummary()
|
||||
private function displaySummary(): void
|
||||
{
|
||||
$builder = new Builder(
|
||||
writer: new PngWriter(),
|
||||
@@ -163,6 +162,7 @@ class PaymentPdf extends Fpdf
|
||||
labelFont: new OpenSans(30),
|
||||
labelAlignment: LabelAlignment::Center
|
||||
);
|
||||
|
||||
$result = $builder->build();
|
||||
|
||||
$tmpname = Uuid::v4() . ".png";
|
||||
@@ -170,17 +170,14 @@ class PaymentPdf extends Fpdf
|
||||
$result->saveToFile($dir);
|
||||
|
||||
$this->Image($dir, 25, 235, 35, 35);
|
||||
@unlink($dir); // Clean up the temp file
|
||||
@unlink($dir);
|
||||
|
||||
// Calculate totals
|
||||
$totalHT = array_sum(array_column($this->items, 'priceHt'));
|
||||
$totalTVA = $totalHT * 0.20;
|
||||
$totalTTC = $totalHT + $totalTVA;
|
||||
|
||||
// Position the summary block at the bottom of the page
|
||||
$this->SetY(-60);
|
||||
|
||||
// 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(40, 10, number_format($totalHT, 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<?php
|
||||
namespace App\Service\ResetPassword\Event;
|
||||
|
||||
namespace App\Service\ResetPassword\Event;
|
||||
|
||||
class ResetPasswordConfirmEvent
|
||||
{
|
||||
private string $password;
|
||||
public function __construct(private string $password)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password
|
||||
*/
|
||||
public function setPassword(string $password): void
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<?php
|
||||
namespace App\Service\ResetPassword\Event;
|
||||
|
||||
namespace App\Service\ResetPassword\Event;
|
||||
|
||||
class ResetPasswordEvent
|
||||
{
|
||||
private string $email;
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
*/
|
||||
public function setEmail(string $email): void
|
||||
public function __construct(private string $email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
namespace App\Service\ResetPassword;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Service\Generator\TempPasswordGenerator;
|
||||
use App\Service\Mailer\Event\CreatedAdminEvent;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: ResetPasswordEvent::class, method: 'onResetPassword')]
|
||||
class ResetPasswordPubscriber
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly EntityManagerInterface $entityManager,private readonly Mailer $mailer)
|
||||
{
|
||||
}
|
||||
|
||||
public function onResetPassword(ResetPasswordEvent $resetPasswordEvent)
|
||||
{
|
||||
$email = $resetPasswordEvent->getEmail();
|
||||
$account = $this->entityManager->getRepository(Account::class)->findOneBy(['email'=>$email]);
|
||||
if($account instanceof Account) {
|
||||
$checkExit = $this->entityManager->getRepository(AccountResetPasswordRequest::class)->findOneBy(['Account'=>$account]);
|
||||
$sendNewRequest = true;
|
||||
$t = new \DateTimeImmutable();
|
||||
$request = null;
|
||||
if($checkExit instanceof AccountResetPasswordRequest) {
|
||||
$expiredAt = $checkExit->getExpiresAt();
|
||||
if ($expiredAt < $t) {
|
||||
$this->entityManager->remove($checkExit);
|
||||
} else {
|
||||
$sendNewRequest = false;
|
||||
$request = $checkExit;
|
||||
}
|
||||
}
|
||||
if($sendNewRequest) {
|
||||
$expiredAt = $t->modify("+1 hours");
|
||||
$request = new AccountResetPasswordRequest();
|
||||
$request->setAccount($account);
|
||||
$request->setToken(TempPasswordGenerator::generate(50, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
|
||||
$request->setRequestedAt($t);
|
||||
$request->setExpiresAt($expiredAt);
|
||||
$this->entityManager->persist($request);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$this->mailer->send($account->getEmail(),$account->getUsername(),"[Mainframe] - Lien pour réinitialiser votre mot de passe","mails/artemis/reset.twig",[
|
||||
'account'=>$account,
|
||||
'request'=>$request,
|
||||
'resetLink' => $this->urlGenerator->generate('app_forgotpassword_confirm',['id'=>$account->getId(),'token'=>$request->getToken()],UrlGeneratorInterface::ABSOLUTE_URL),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/Service/ResetPassword/ResetPasswordSubscriber.php
Normal file
81
src/Service/ResetPassword/ResetPasswordSubscriber.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\ResetPassword;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Service\Generator\TempPasswordGenerator;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: ResetPasswordEvent::class, method: 'onResetPassword')]
|
||||
class ResetPasswordSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Mailer $mailer
|
||||
) {
|
||||
}
|
||||
|
||||
public function onResetPassword(ResetPasswordEvent $resetPasswordEvent): void
|
||||
{
|
||||
$email = $resetPasswordEvent->getEmail();
|
||||
$account = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]);
|
||||
|
||||
if (!$account instanceof Account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingRequest = $this->entityManager->getRepository(AccountResetPasswordRequest::class)
|
||||
->findOneBy(['account' => $account]);
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
$sendNewRequest = true;
|
||||
$request = null;
|
||||
|
||||
if ($existingRequest instanceof AccountResetPasswordRequest) {
|
||||
$expiredAt = $existingRequest->getExpiresAt();
|
||||
if ($expiredAt < $now) {
|
||||
$this->entityManager->remove($existingRequest);
|
||||
} else {
|
||||
$sendNewRequest = false;
|
||||
$request = $existingRequest;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sendNewRequest) {
|
||||
$expiredAt = $now->modify('+1 hour');
|
||||
|
||||
$request = new AccountResetPasswordRequest();
|
||||
$request->setAccount($account);
|
||||
$request->setToken(TempPasswordGenerator::generate(50, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'));
|
||||
$request->setRequestedAt($now);
|
||||
$request->setExpiresAt($expiredAt);
|
||||
|
||||
$this->entityManager->persist($request);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$resetLink = $this->urlGenerator->generate(
|
||||
'app_forgotpassword_confirm',
|
||||
['id' => $account->getId(), 'token' => $request->getToken()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
|
||||
$this->mailer->send(
|
||||
$account->getEmail(),
|
||||
$account->getUsername(),
|
||||
'[Mainframe] - Lien pour réinitialiser votre mot de passe',
|
||||
'mails/artemis/reset.twig',
|
||||
[
|
||||
'account' => $account,
|
||||
'request' => $request,
|
||||
'resetLink' => $resetLink,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,48 +10,74 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class RevendeurService
|
||||
{
|
||||
private const ZONE_ID = '7b8ae9e2a488d574b19dfa72a67326d9';
|
||||
private const DNS_SUFFIX = '-demande.esy-web.fr';
|
||||
private const DNS_CONTENT_IP = '35.204.191.160';
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly RevendeurRepository $revendeurRepository,
|
||||
private readonly HttpClientInterface $httpClient
|
||||
)
|
||||
{
|
||||
private readonly RevendeurRepository $revendeurRepository,
|
||||
private readonly HttpClientInterface $httpClient
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des revendeurs avec le champ dns ajouté.
|
||||
*
|
||||
* @return Revendeur[]
|
||||
*/
|
||||
public function list(): array
|
||||
{
|
||||
$lists = [];
|
||||
foreach ($this->revendeurRepository->findAll() as $list) {
|
||||
$list->dns = $list->getCode() . "-demande.esy-web.fr";
|
||||
$lists[] = $list;
|
||||
foreach ($this->revendeurRepository->findAll() as $revendeur) {
|
||||
// Ajout dynamique du champ dns
|
||||
$revendeur->dns = $revendeur->getCode() . self::DNS_SUFFIX;
|
||||
$lists[] = $revendeur;
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
public function create(\App\Entity\Revendeur $r)
|
||||
/**
|
||||
* Crée un revendeur en base et ajoute un enregistrement DNS Cloudflare.
|
||||
*
|
||||
* @param Revendeur $revendeur
|
||||
* @throws TransportExceptionInterface en cas d’erreur HTTP
|
||||
*/
|
||||
public function create(Revendeur $revendeur): void
|
||||
{
|
||||
$this->entityManager->persist($r);
|
||||
$this->entityManager->persist($revendeur);
|
||||
$this->entityManager->flush();
|
||||
$dns = $r->getCode() . "-demande.esy-web.fr";
|
||||
$ZONE_ID = "7b8ae9e2a488d574b19dfa72a67326d9";
|
||||
$this->httpClient->request('POST', 'https://api.cloudflare.com/client/v4/zones/' . $ZONE_ID . '/dns_records', [
|
||||
"headers" => [
|
||||
|
||||
$dns = $revendeur->getCode() . self::DNS_SUFFIX;
|
||||
$token = $_ENV['CLOUDFLARE_TOKEN'] ?? '';
|
||||
|
||||
$this->httpClient->request('POST', sprintf(
|
||||
'https://api.cloudflare.com/client/v4/zones/%s/dns_records',
|
||||
self::ZONE_ID
|
||||
), [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $_ENV['CLOUDFLARE_TOKEN'],
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
],
|
||||
'json' => [
|
||||
'name' => $dns,
|
||||
'ttl' => 1,
|
||||
'type' => 'A',
|
||||
'comment' => 'Link for ' . $r->getRaisonSocial() . " for created",
|
||||
'content' => '35.204.191.160',
|
||||
'comment' => 'Link for ' . $revendeur->getRaisonSocial() . ' for created',
|
||||
'content' => self::DNS_CONTENT_IP,
|
||||
'proxied' => true,
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function get($id) : Revendeur
|
||||
/**
|
||||
* Récupère un revendeur par son ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Revendeur|null
|
||||
*/
|
||||
public function get(int $id): ?Revendeur
|
||||
{
|
||||
return $this->revendeurRepository->find($id);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Revendeur;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: SendLinkEvent::class, method: 'onSendLinkEvent')]
|
||||
class RevendeurSubcriber
|
||||
{
|
||||
public function __construct(private readonly Mailer $mailer)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSendLinkEvent(SendLinkEvent $sendLinkEvent)
|
||||
{
|
||||
$revendeur = $sendLinkEvent->getRevendeur();
|
||||
$this->mailer->send($revendeur->getEmail(),$revendeur->getRaisonSocial(),"[ESY-WEB] - Lien pour vos demande d'ouverture du site","mails/revendeur/link.twig",[
|
||||
"url" => $revendeur->getCode()."-demande.esy-web.fr",
|
||||
'revendeur' => $revendeur
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
29
src/Service/Revendeur/RevendeurSubscriber.php
Normal file
29
src/Service/Revendeur/RevendeurSubscriber.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Revendeur;
|
||||
|
||||
use App\Service\Mailer\Mailer;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: SendLinkEvent::class, method: 'onSendLinkEvent')]
|
||||
class RevendeurSubscriber
|
||||
{
|
||||
public function __construct(private readonly Mailer $mailer)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSendLinkEvent(SendLinkEvent $event): void
|
||||
{
|
||||
$revendeur = $event->getRevendeur();
|
||||
$subject = '[ESY-WEB] - Lien pour vos demandes d\'ouverture du site';
|
||||
$template = 'mails/revendeur/link.twig';
|
||||
$context = [
|
||||
'url' => $revendeur->getCode() . '-demande.esy-web.fr',
|
||||
'revendeur' => $revendeur,
|
||||
];
|
||||
|
||||
$this->mailer->send($revendeur->getEmail(), $revendeur->getRaisonSocial(), $subject, $template, $context);
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,12 @@ use App\Entity\Revendeur;
|
||||
|
||||
class SendLinkEvent
|
||||
{
|
||||
private Revendeur $revendeur;
|
||||
|
||||
public function __construct(Revendeur $revendeur)
|
||||
public function __construct(private readonly Revendeur $revendeur)
|
||||
{
|
||||
$this->revendeur = $revendeur;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Revendeur
|
||||
*/
|
||||
public function getRevendeur(): Revendeur
|
||||
{
|
||||
return $this->revendeur;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Service\Stancer;
|
||||
|
||||
use App\Entity\Customer as AppCustomer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stancer\Config;
|
||||
use Stancer\Customer;
|
||||
@@ -12,23 +13,40 @@ class Client
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->client = Config::init([$_ENV['STANCER_PUBLIC_KEY'], $_ENV['STANCER_PRIVATE_KEY']]);
|
||||
$this->client->setMode($_ENV['STANCER_ENV']);
|
||||
$publicKey = $_ENV['STANCER_PUBLIC_KEY'] ?? '';
|
||||
$privateKey = $_ENV['STANCER_PRIVATE_KEY'] ?? '';
|
||||
$env = $_ENV['STANCER_ENV'] ?? 'test';
|
||||
|
||||
$this->client = Config::init([$publicKey, $privateKey]);
|
||||
$this->client->setMode($env);
|
||||
}
|
||||
|
||||
public function customer(?\App\Entity\Customer $customer)
|
||||
/**
|
||||
* Récupère ou crée un client Stancer à partir d'un customer interne.
|
||||
*
|
||||
* @param AppCustomer|null $customer
|
||||
* @return string|array|null Identifiant Stancer ou tableau décodé du résultat de création, ou null si absence de customer
|
||||
*/
|
||||
public function customer(?AppCustomer $customer): string|array|null
|
||||
{
|
||||
if ($customer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if($customer->getStancerId() == null) {
|
||||
$stancerId = $customer->getStancerId();
|
||||
|
||||
if ($stancerId === null) {
|
||||
$customerItem = new Customer();
|
||||
$customerItem->setEmail($customer->mainContact()->getEmail());
|
||||
$mainContact = $customer->mainContact();
|
||||
|
||||
$customerItem->setEmail($mainContact->getEmail());
|
||||
$customerItem->setName($customer->getRaisonSocial());
|
||||
|
||||
$result = $customerItem->send();
|
||||
|
||||
return json_decode($result, true);
|
||||
}
|
||||
|
||||
return $customer->getStancerId();
|
||||
|
||||
return $stancerId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Service\Vault;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class VaultClient
|
||||
{
|
||||
@@ -12,13 +12,13 @@ class VaultClient
|
||||
|
||||
private const KEYS = [
|
||||
'mainframe_logger',
|
||||
'mainframe_customer'
|
||||
'mainframe_customer',
|
||||
];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $httpClient)
|
||||
{
|
||||
$this->vaultAddr = rtrim($_ENV['VAULT_ADDR'], '/');
|
||||
$this->vaultToken = $_ENV['VAULT_TOKEN'];
|
||||
$this->vaultAddr = rtrim($_ENV['VAULT_ADDR'] ?? '', '/');
|
||||
$this->vaultToken = $_ENV['VAULT_TOKEN'] ?? '';
|
||||
|
||||
$this->ensureTransitEnabled();
|
||||
$this->ensureKeysExist();
|
||||
@@ -39,8 +39,9 @@ class VaultClient
|
||||
]);
|
||||
|
||||
$data = $response->toArray(false);
|
||||
|
||||
return $data['data']['ciphertext'] ?? null;
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -60,8 +61,13 @@ class VaultClient
|
||||
]);
|
||||
|
||||
$data = $response->toArray(false);
|
||||
return isset($data['data']['plaintext']) ? base64_decode($data['data']['plaintext']) : null;
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
|
||||
if (isset($data['data']['plaintext'])) {
|
||||
return base64_decode($data['data']['plaintext']);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (TransportExceptionInterface) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +87,7 @@ class VaultClient
|
||||
|
||||
if (!isset($data['transit/'])) {
|
||||
$enableUrl = sprintf('%s/v1/sys/mounts/transit', $this->vaultAddr);
|
||||
|
||||
$this->httpClient->request('POST', $enableUrl, [
|
||||
'headers' => [
|
||||
'X-Vault-Token' => $this->vaultToken,
|
||||
@@ -92,7 +99,7 @@ class VaultClient
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +121,8 @@ class VaultClient
|
||||
'X-Vault-Token' => $this->vaultToken,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $exception) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
// Si la clé n'existe pas, la créer
|
||||
$this->httpClient->request('POST', $url, [
|
||||
'headers' => [
|
||||
'X-Vault-Token' => $this->vaultToken,
|
||||
@@ -136,8 +144,9 @@ class VaultClient
|
||||
'X-Vault-Token' => $this->vaultToken,
|
||||
],
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
} catch (TransportExceptionInterface) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
12
total.sh
Executable file
12
total.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
WEBHOOK_URL="https://discord.com/api/webhooks/1421437443688890500/DPSuS00Ian6O0lQw-1aDQPkVB19so4AA5zaLN_nALs3fDGah1KSLBWys_CYpsc33PGIG"
|
||||
TODAY=$(date +"%Y-%m-%d")
|
||||
COMMITS_COUNT=$(git log --since="$TODAY 00:00" --until="$TODAY 23:59" --oneline | wc -l)
|
||||
|
||||
MESSAGE="Date : $TODAY
|
||||
Nombre de commits aujourd'hui : $COMMITS_COUNT"
|
||||
|
||||
JSON_PAYLOAD=$(jq -n --arg content "$MESSAGE" '{content: $content}')
|
||||
|
||||
curl -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"
|
||||
Reference in New Issue
Block a user