feat(AvisPaymentState): Ajoute champ isRecover pour suivi des relances.

 feat(Customer): Ajoute relation avec FaultPayment pour suivi des impayés.
🐛 fix(CheckAvisPaymentStateCommand): Crée FaultPayment et relance si nécessaire.
🎨 style(customer.twig): Affiche si le client a des factures impayées.
`
This commit is contained in:
Serreau Jovann
2025-11-13 14:33:40 +01:00
parent de53ac88ad
commit aa1910d6f5
8 changed files with 253 additions and 7 deletions

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251113132634 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE fault_payment (id SERIAL NOT NULL, customer_id INT DEFAULT NULL, id_advert_payment INT NOT NULL, entry_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_873651E99395C3F3 ON fault_payment (customer_id)');
$this->addSql('COMMENT ON COLUMN fault_payment.entry_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE fault_payment ADD CONSTRAINT FK_873651E99395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
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 fault_payment DROP CONSTRAINT FK_873651E99395C3F3');
$this->addSql('DROP TABLE fault_payment');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20251113133056 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 is_recover BOOLEAN 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 is_recover');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Command;
use App\Entity\FaultPayment;
use App\Repository\AvisPaymentStateRepository;
use App\Service\Customer\Billing\CreateAvisEventSend;
use App\Service\Customer\Billing\CreateAvisEventSend2nd;
@@ -70,14 +71,27 @@ class CheckAvisPaymentStateCommand extends Command
$this->entityManager->persist($avisPaymentState);
$evDevis = new CreateAvisEventSendFinal($avisPaymentState->getAvisPayment());
$this->eventDispatcher->dispatch($evDevis);
}
}
/* foreach ($this->avisPaymentStateRepository->findAll() as $avisPaymentState) {
$evDevis = new CreateAvisEventSendRecover($avisPaymentState->getAvisPayment());
$this->eventDispatcher->dispatch($evDevis);
}*/
foreach ($this->avisPaymentStateRepository->findBy(['isFinalSend'=>true,'isRecover'=>null]) as $avisPaymentState) {
$paymentFault = $this->entityManager->getRepository(FaultPayment::class)->findOneBy([
'customer' => $avisPaymentState->getAvisPayment()->getCustomer(),
'idAdvertPayment' => $avisPaymentState->getAvisPayment()->getId()
]);
if(!$paymentFault instanceof FaultPayment){
$avisPaymentState->setIsRecover(true);
$this->entityManager->persist($avisPaymentState);
$paymentFault = new FaultPayment();
$paymentFault->setCustomer($avisPaymentState->getAvisPayment()->getCustomer());
$paymentFault->setIdAdvertPayment($avisPaymentState->getAvisPayment()->getId());
$paymentFault->setEntryAt(new \DateTimeImmutable());
$this->entityManager->persist($paymentFault);
$this->entityManager->flush();
$evDevis = new CreateAvisEventSendRecover($avisPaymentState->getAvisPayment());
$this->eventDispatcher->dispatch($evDevis);
}
}
$this->entityManager->flush();

View File

@@ -58,6 +58,9 @@ class AvisPaymentState
#[ORM\Column(length: 255, nullable: true)]
private ?string $month = null;
#[ORM\Column(nullable: true)]
private ?bool $isRecover = null;
public function getId(): ?int
{
return $this->id;
@@ -242,4 +245,16 @@ class AvisPaymentState
return $this;
}
public function isRecover(): ?bool
{
return $this->isRecover;
}
public function setIsRecover(?bool $isRecover): static
{
$this->isRecover = $isRecover;
return $this;
}
}

View File

@@ -126,6 +126,12 @@ class Customer
#[ORM\OneToOne(mappedBy: 'customer', cascade: ['persist', 'remove'])]
private ?CustomerWallet $customerWallet = null;
/**
* @var Collection<int, FaultPayment>
*/
#[ORM\OneToMany(targetEntity: FaultPayment::class, mappedBy: 'customer')]
private Collection $faultPayments;
public function __clone(): void
{
@@ -150,6 +156,7 @@ class Customer
$this->websites = new ArrayCollection();
$this->customerSplits = new ArrayCollection();
$this->customerSepas = new ArrayCollection();
$this->faultPayments = new ArrayCollection();
}
public function getId(): ?int
@@ -658,4 +665,34 @@ class Customer
return $this;
}
/**
* @return Collection<int, FaultPayment>
*/
public function getFaultPayments(): Collection
{
return $this->faultPayments;
}
public function addFaultPayment(FaultPayment $faultPayment): static
{
if (!$this->faultPayments->contains($faultPayment)) {
$this->faultPayments->add($faultPayment);
$faultPayment->setCustomer($this);
}
return $this;
}
public function removeFaultPayment(FaultPayment $faultPayment): static
{
if ($this->faultPayments->removeElement($faultPayment)) {
// set the owning side to null (unless already changed)
if ($faultPayment->getCustomer() === $this) {
$faultPayment->setCustomer(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Repository\FaultPaymentRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: FaultPaymentRepository::class)]
class FaultPayment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'faultPayments')]
private ?Customer $customer = null;
#[ORM\Column]
private ?int $idAdvertPayment = null;
#[ORM\Column]
private ?\DateTimeImmutable $entryAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getCustomer(): ?Customer
{
return $this->customer;
}
public function setCustomer(?Customer $customer): static
{
$this->customer = $customer;
return $this;
}
public function getIdAdvertPayment(): ?int
{
return $this->idAdvertPayment;
}
public function setIdAdvertPayment(int $idAdvertPayment): static
{
$this->idAdvertPayment = $idAdvertPayment;
return $this;
}
public function getEntryAt(): ?\DateTimeImmutable
{
return $this->entryAt;
}
public function setEntryAt(\DateTimeImmutable $entryAt): static
{
$this->entryAt = $entryAt;
return $this;
}
}

View File

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

View File

@@ -73,7 +73,11 @@
<td class="px-6 py-4 text-center text-sm">{{ customer.customerDns.count }}</td>
<td class="px-6 py-4 text-center text-sm">{{ customer|countEmail }}</td>
<td class="px-6 py-4 text-center text-sm">0</td>
<td class="px-6 py-4 text-center text-sm text-green-500 font-bold">Non</td>
{% if customer.faultPayments.count > 0 %}
<td class="px-6 py-4 text-center text-sm text-red-500 font-bold">OUI ({{ customer.faultPayments.count}} facture en retard)</td>
{% else %}
<td class="px-6 py-4 text-center text-sm text-green-500 font-bold">Non</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Voir</a>
<a data-turbo="false" is="confirm-modal" type="cp-customer" href="{{ path('artemis_intranet_customer',{idCopy:customer.id}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Copier le client</a>