feat(AvisPaymentState): Ajoute les champs year et month pour le suivi.

🌐 i18n(messages.fr.yaml): Ajoute la traduction pour l'hébergement de site.
 feat(templates/price.twig): Ajoute les prix pour les offres d'hébergement.
 feat(AutoCreatedAvisPaymentCommand): Génère les avis de paiement pour l'hébergement.
♻️ refactor(PriceController): Gère la sauvegarde des prix d'hébergement.
📧 feat(avis-payment-wait.twig): Améliore le formatage du mail d'avis de paiement.
This commit is contained in:
Serreau Jovann
2025-11-12 19:02:23 +01:00
parent f008903b8c
commit 0fa2bbaee8
7 changed files with 318 additions and 3 deletions

View File

@@ -0,0 +1,34 @@
<?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 Version20251112173808 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 avis_payment_state ADD year VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE avis_payment_state ADD month 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 avis_payment_state DROP year');
$this->addSql('ALTER TABLE avis_payment_state DROP month');
}
}

View File

@@ -6,10 +6,13 @@ use App\Entity\AvisPaymentState;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentLine;
use App\Entity\CustomerDns;
use App\Entity\EsyWeb\WebsiteDates;
use App\Repository\AvisPaymentStateRepository;
use App\Repository\CustomerAdvertPaymentRepository;
use App\Repository\CustomerDnsRepository;
use App\Repository\CustomerPriceRepository;
use App\Repository\EsyWeb\WebsiteDatesRepository;
use App\Repository\EsyWeb\WebsiteRepository;
use App\Service\Customer\CreateAvisEvent;
use App\Service\Ovh\Client;
use Doctrine\ORM\EntityManagerInterface;
@@ -29,6 +32,8 @@ class AutoCreatedAvisPaymentCommand extends Command
private CustomerAdvertPaymentRepository $customerAdvertPaymentRepository,
private AvisPaymentStateRepository $avisPaymentStateRepository,
private EntityManagerInterface $entityManager,
private WebsiteDatesRepository $websiteDatesRepository,
private WebsiteRepository $websiteRepository,
private CustomerPriceRepository $customerPriceRepository,
private EventDispatcherInterface $eventDispatcher,
?string $name = null)
@@ -42,9 +47,10 @@ class AutoCreatedAvisPaymentCommand extends Command
$io->title("Auto Generated Avis Payment");
$nddRenouvellement = $this->customerPriceRepository->findOneBy(['type'=>'renouvellement_ndd']);
$nddGestion = $this->customerPriceRepository->findOneBy(['type'=>'gestion_ndd']);
$t = new \DateTime();
/** @var CustomerDns $value */
foreach ($this->customerDnsRepository->expitedSoon() as $value) {
$searchCurrentStat = $this->avisPaymentStateRepository->findOneBy(['type'=>'ndd','target'=>$value->getNdd()]);
$searchCurrentStat = $this->avisPaymentStateRepository->findOneBy(['type'=>'ndd','target'=>$value->getNdd(),'year'=>$t->format('Y')]);
if(!$searchCurrentStat){
$t = new \DateTimeImmutable();
$num = "A-".$t->format('Y/m')."/".sprintf('%05d',$this->customerAdvertPaymentRepository->count()+1);
@@ -107,9 +113,10 @@ class AutoCreatedAvisPaymentCommand extends Command
$tThird = $tThird->modify("+2 week");
$tFinal = new \DateTimeImmutable();
$tFinal = $tFinal->modify("+1 month");
$tThird = $tFinal->modify("+2 week");
$tFinal = $tFinal->modify("+2 week");
$tFinal = $tFinal->modify("+4 week");
$avisState = new AvisPaymentState();
$avisState->setYear($t->format('Y'));
$avisState->setAvisPayment($avisPayment);
$avisState->setFirstSendAt($tFirst);
$avisState->setSecondSendAt($tSecond);
@@ -134,6 +141,93 @@ class AutoCreatedAvisPaymentCommand extends Command
}
}
$hosting1 = $this->customerPriceRepository->findOneBy(['type'=>'hosting_1']);
$hosting2 = $this->customerPriceRepository->findOneBy(['type'=>'hosting_2']);
$hostingRestore = $this->customerPriceRepository->findOneBy(['type'=>'hosting_restore']);
foreach ($this->websiteRepository->findBy(['state' =>'deploy']) as $value) {
if($value->getOffert() == "Esy-Premium" || $value->getOffert() == "E-COMMERCE - PREMIUM") {
$dates = $value->getWebsiteDates()->getNextHosting();
$t = new \DateTimeImmutable();
$diff = $t->diff($dates);
if($diff->days <= 60) {
$searchCurrentStat = $this->avisPaymentStateRepository->findOneBy(['type'=>'hosting','target'=>$value->getUuid(),'year'=>$t->format('Y')]);
if(!$searchCurrentStat) {
$t = new \DateTimeImmutable();
$num = "A-" . $t->format('Y/m') . "/" . sprintf('%05d', $this->customerAdvertPaymentRepository->count() + 1);
$io->info("Generate avis payment - Hosting " . $value->getTitle()." - EsyPremium");
$avisPayment = new CustomerAdvertPayment();
$avisPayment->setState("created");
$avisPayment->setCustomer($value->getCustomer());
$avisPayment->setCreateAt(new \DateTimeImmutable('now'));
$avisPayment->setUpdateAt(new \DateTimeImmutable('now'));
$avisPayment->setNumAvis($num);
$this->entityManager->persist($avisPayment);
$expiredAt = $value->getWebsiteDates()->getNextHosting();
$expiredAt = $expiredAt->modify("-1 year");
$newLimitAt = clone $value->getWebsiteDates()->getNextHosting();
if($value->getType() =="vitrine") {
$desciption = $hosting1->getDescription();
$price = $hosting1->getAmmount();
} else {
$desciption = $hosting2->getDescription();
$price = $hosting2->getAmmount();
}
$content = explode("\n", $desciption);
$title = str_replace("{website_name}",$value->getMainDns(), $content[0]);
unset($content[0]);
$desciption = implode("\n", $content);
$desciption = str_replace("{date_start}",$expiredAt->format('d M Y'),$desciption);
$desciption = str_replace("{date_stop}",$newLimitAt->format('d M Y'),$desciption);
$avisLine = new CustomerAdvertPaymentLine();
$avisLine->setPos(0);
$avisLine->setName($title);
$avisLine->setPriceHT($price);
$avisLine->setContent($desciption);
$avisLine->setTva(1.20);
$this->entityManager->persist($avisLine);
$avisPayment->addCustomerAdvertPaymentLine($avisLine);
$this->entityManager->persist($avisPayment);
$tFirst = new \DateTimeImmutable();
$tSecond = new \DateTimeImmutable();
$tSecond = $tSecond->modify("+1 month");
$tThird = new \DateTimeImmutable();
$tThird = $tThird->modify("+1 month");
$tThird = $tThird->modify("+2 week");
$tFinal = new \DateTimeImmutable();
$tFinal = $tFinal->modify("+1 month");
$tFinal = $tFinal->modify("+2 week");
$tFinal = $tFinal->modify("+4 week");
$avisState = new AvisPaymentState();
$avisState->setYear($t->format('Y'));
$avisState->setAvisPayment($avisPayment);
$avisState->setFirstSendAt($tFirst);
$avisState->setSecondSendAt($tSecond);
$avisState->setThirdSendAt($tThird);
$avisState->setFinalSendAt($tFinal);
$avisState->setType("hosting");
$avisState->setYear($t->format('Y'));
$avisState->setTarget($value->getUuid());
$avisState->setIsGenerated(true);
$avisState->setIsValidated(false);
$avisState->setIsFirstSend(false);
$avisState->setIsSecondSend(false);
$avisState->setIsThirdSend(false);
$avisState->setIsFinalSend(false);
$this->entityManager->persist($avisState);
$this->entityManager->flush();
$event = new CreateAvisEvent($avisPayment, false);
$this->eventDispatcher->dispatch($event);
$io->info("Completed avis payement generated");
}
}
}
}

View File

@@ -107,6 +107,42 @@ class PriceController extends AbstractController
$entityManager->flush();
return $this->redirectToRoute('artemis_intranet_price');
}
if(isset($data['price_hosting_1'])) {
$customPrice = $customerPriceRepository->findOneBy(['type'=>'hosting_1']);
if(!$customPrice) {
$customPrice = new CustomerPrice();
$customPrice->setType('hosting_1');
}
$customPrice->setDescription($data['description_hosting_1']);
$customPrice->setAmmount(floatval($data['price_hosting_1']));
$entityManager->persist($customPrice);
$entityManager->flush();
return $this->redirectToRoute('artemis_intranet_price');
}
if(isset($data['price_hosting_2'])) {
$customPrice = $customerPriceRepository->findOneBy(['type'=>'hosting_2']);
if(!$customPrice) {
$customPrice = new CustomerPrice();
$customPrice->setType('hosting_2');
}
$customPrice->setDescription($data['description_hosting_2']);
$customPrice->setAmmount(floatval($data['price_hosting_2']));
$entityManager->persist($customPrice);
$entityManager->flush();
return $this->redirectToRoute('artemis_intranet_price');
}
if(isset($data['price_hosting_restore'])) {
$customPrice = $customerPriceRepository->findOneBy(['type'=>'hosting_restore']);
if(!$customPrice) {
$customPrice = new CustomerPrice();
$customPrice->setType('hosting_restore');
}
$customPrice->setDescription($data['description_hosting_restore']);
$customPrice->setAmmount(floatval($data['price_hosting_restore']));
$entityManager->persist($customPrice);
$entityManager->flush();
return $this->redirectToRoute('artemis_intranet_price');
}
if(isset($data['price_ndd_gestion'])) {
$customPrice = $customerPriceRepository->findOneBy(['type'=>'gestion_ndd']);
if(!$customPrice) {
@@ -127,6 +163,9 @@ class PriceController extends AbstractController
'renouvellement_ndd' => $customerPriceRepository->findOneBy(['type'=>'renouvellement_ndd']),
'restore_ndd' => $customerPriceRepository->findOneBy(['type'=>'restore_ndd']),
'gestion_ndd' => $customerPriceRepository->findOneBy(['type'=>'gestion_ndd']),
'hosting_1' => $customerPriceRepository->findOneBy(['type'=>'hosting_1']),
'hosting_2' => $customerPriceRepository->findOneBy(['type'=>'hosting_2']),
'hosting_restore' => $customerPriceRepository->findOneBy(['type'=>'hosting_restore']),
]);
}
}

View File

@@ -52,6 +52,12 @@ class AvisPaymentState
#[ORM\Column]
private ?\DateTimeImmutable $finalSendAt = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $year = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $month = null;
public function getId(): ?int
{
return $this->id;
@@ -212,4 +218,28 @@ class AvisPaymentState
return $this;
}
public function getYear(): ?string
{
return $this->year;
}
public function setYear(?string $year): static
{
$this->year = $year;
return $this;
}
public function getMonth(): ?string
{
return $this->month;
}
public function setMonth(?string $month): static
{
$this->month = $month;
return $this;
}
}

View File

@@ -158,6 +158,123 @@
</button>
</form>
</div>
<div class="bg-white dark:bg-gray-800 p-6 md:p-8 rounded-xl shadow-2xl transition duration-300">
<h2 class="text-1xl font-bold text-indigo-700 dark:text-indigo-400 mb-4">Hébergement 5 Go</h2>
<form action="#" method="POST" class="space-y-6">
<!-- Champ Prix -->
<div>
<label for="price_hosting_1" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Prix (€)</label>
<div class="relative mt-1 rounded-md shadow-sm">
<input type="number" name="price_hosting_1" id="price_hosting_1" value="{% if hosting_1 is not null %}{{ hosting_1.ammount }}{% endif %}" step="0.01" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 pl-7 pr-12 py-2 text-gray-900 dark:text-white placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00">
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span class="text-gray-500 dark:text-gray-400 sm:text-sm">€</span>
</div>
</div>
</div>
<!-- Champ Description -->
<div>
<label for="description_hosting_1" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea id="description_hosting_1" name="description_hosting_1" rows="3" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-sm p-3 text-gray-900 dark:text-white focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="">{% if hosting_1 is not null %}{{ hosting_1.description }}{% endif %}</textarea>
</div>
<div class="row">
<span class="badge bg-indigo-500 rounded p-1">{website_name}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_start}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_stop}</span>
</div>
<!-- Bouton d'action -->
<button type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out">
Sauvegarder
</button>
</form>
</div>
<div class="bg-white dark:bg-gray-800 p-6 md:p-8 rounded-xl shadow-2xl transition duration-300">
<h2 class="text-1xl font-bold text-indigo-700 dark:text-indigo-400 mb-4">Hébergement 10 Go</h2>
<form action="#" method="POST" class="space-y-6">
<!-- Champ Prix -->
<div>
<label for="price_hosting_2" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Prix (€)</label>
<div class="relative mt-1 rounded-md shadow-sm">
<input type="number" name="price_hosting_2" id="price_hosting_2" value="{% if hosting_2 is not null %}{{ hosting_2.ammount }}{% endif %}" step="0.01" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 pl-7 pr-12 py-2 text-gray-900 dark:text-white placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00">
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span class="text-gray-500 dark:text-gray-400 sm:text-sm">€</span>
</div>
</div>
</div>
<!-- Champ Description -->
<div>
<label for="description_hosting_2" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea id="description_hosting_2" name="description_hosting_2" rows="3" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-sm p-3 text-gray-900 dark:text-white focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="">{% if hosting_2 is not null %}{{ hosting_2.description }}{% endif %}</textarea>
</div>
<div class="row">
<span class="badge bg-indigo-500 rounded p-1">{website_name}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_start}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_stop}</span>
</div>
<!-- Bouton d'action -->
<button type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out">
Sauvegarder
</button>
</form>
</div>
<div class="bg-white dark:bg-gray-800 p-6 md:p-8 rounded-xl shadow-2xl transition duration-300">
<h2 class="text-1xl font-bold text-indigo-700 dark:text-indigo-400 mb-4">Restauration Hébergement</h2>
<form action="#" method="POST" class="space-y-6">
<!-- Champ Prix -->
<div>
<label for="price_hosting_restore" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Prix (€)</label>
<div class="relative mt-1 rounded-md shadow-sm">
<input type="number" name="price_hosting_restore" id="price_hosting_restore" value="{% if hosting_restore is not null %}{{ hosting_restore.ammount }}{% endif %}" step="0.01" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 pl-7 pr-12 py-2 text-gray-900 dark:text-white placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00">
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span class="text-gray-500 dark:text-gray-400 sm:text-sm">€</span>
</div>
</div>
</div>
<!-- Champ Description -->
<div>
<label for="description_hosting_restore" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea id="description_hosting_restore" name="description_hosting_restore" rows="3" required
class="block w-full rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-sm p-3 text-gray-900 dark:text-white focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="">{% if hosting_restore is not null %}{{ hosting_restore.description }}{% endif %}</textarea>
</div>
<div class="row">
<span class="badge bg-indigo-500 rounded p-1">{website_name}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_start}</span>
<span class="badge bg-indigo-500 rounded p-1">{date_stop}</span>
</div>
<!-- Bouton d'action -->
<button type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out">
Sauvegarder
</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@
Bonjour,
</mj-text>
<mj-text>
Nous vous informons qu'un nouvel avis de paiement numéro **{{ datas.paymentNotice.number }}** d'un montant de **{{ datas.paymentNotice.amount | number_format(2, ',', ' ') }} € TTC ** est disponible.
Nous vous informons qu'un nouvel avis de paiement numéro <strong>{{ datas.paymentNotice.number }}</strong> d'un montant de <strong>{{ datas.paymentNotice.amount | number_format(2, ',', ' ') }} € TTC</strong> est disponible.
Vous pouvez le consulter et procéder au paiement en cliquant sur le bouton ci-dessous.
</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">

View File

@@ -84,5 +84,6 @@ esyWeb_created: Créer en attends de validation
esyWeb_validate: Validation - En Attends de déploiement
esyWeb_deploy: En ligne
vadvert-ndd : Renouvellement nom de domaine
vadvert-hosting : Hébergement de votre site internet
f-created: Facture Crée
f-send: Facture envoyée