feat(Customer/Billing): Crée l'événement de génération d'avis de paiement et PDF.

This commit is contained in:
Serreau Jovann
2025-07-29 16:10:15 +02:00
parent 2437458fbc
commit fe9960ac45
18 changed files with 1014 additions and 2 deletions

4
.env
View File

@@ -60,3 +60,7 @@ OVH_CUSTOMER=56c387eb9ca4b9a2de4d4d97fd3d7f22
DOCUSIGN_URL=https://signature.esy-web.dev/api
DOCUSIGN_KEY=52u82oCoiG79awGsuxLfJqhxYjg8mrJfAsJJHejRMFa
STANCER_PRIVATE_KEY=stest_Rv4Hz8ae2wQdjnBVCays7wPo
STANCER_PUBLIC_KEY=ptest_raV5vZ51Lnp2DfBtu5TVs5o0
STANCER_ENV=test

View File

@@ -13,6 +13,7 @@
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.5",
"docusealco/docuseal-php": "*",
"endroid/qr-code": "*",
"fpdf/fpdf": "*",
"google/cloud": "^0.296.0",
"imagine/imagine": "^1.5",
@@ -33,6 +34,7 @@
"sentry/sentry-symfony": "^5.3",
"setasign/fpdi": "^2.6",
"spatie/mjml-php": "^1.2",
"stancer/stancer": "*",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",

242
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1d76d59e951b53e55af5bbeb42af9bae",
"content-hash": "8bdb67f55f7688464c0ac8ffc06fbbe5",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -157,6 +157,60 @@
},
"time": "2025-07-16T00:32:17+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^8.1"
},
"require-dev": {
"phly/keep-a-changelog": "^2.12",
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
},
"time": "2024-10-01T13:55:55+00:00"
},
{
"name": "brick/math",
"version": "0.13.1",
@@ -457,6 +511,56 @@
],
"time": "2024-09-19T14:15:21+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
},
"time": "2024-08-09T14:30:48+00:00"
},
{
"name": "doctrine/collections",
"version": "2.3.0",
@@ -1687,6 +1791,78 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "endroid/qr-code",
"version": "6.0.9",
"source": {
"type": "git",
"url": "https://github.com/endroid/qr-code.git",
"reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/21e888e8597440b2205e2e5c484b6c8e556bcd1a",
"reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^3.0",
"php": "^8.2"
},
"require-dev": {
"endroid/quality": "dev-main",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"Endroid\\QrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen van den Enden",
"email": "info@endroid.nl"
}
],
"description": "Endroid QR Code",
"homepage": "https://github.com/endroid/qr-code",
"keywords": [
"code",
"endroid",
"php",
"qr",
"qrcode"
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/6.0.9"
},
"funding": [
{
"url": "https://github.com/endroid",
"type": "github"
}
],
"time": "2025-07-13T19:59:45+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
@@ -6533,6 +6709,70 @@
],
"time": "2025-06-13T08:35:04+00:00"
},
{
"name": "stancer/stancer",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://gitlab.com/wearestancer/library/lib-php.git",
"reference": "91ca640ae1e40167fd05a70c8970ece9157ad4bd"
},
"dist": {
"type": "zip",
"url": "https://gitlab.com/api/v4/projects/wearestancer%2Flibrary%2Flib-php/repository/archive.zip?sha=91ca640ae1e40167fd05a70c8970ece9157ad4bd",
"reference": "91ca640ae1e40167fd05a70c8970ece9157ad4bd",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/http-message": "^2.0",
"psr/log": "^3.0"
},
"require-dev": {
"atoum/atoum": "^4.2",
"atoum/stubs": "^2.6",
"atoum/visibility-extension": "^2.0",
"fakerphp/faker": "^1.13",
"guzzlehttp/guzzle": "^7.2",
"jdslv/atoum-report-cobertura": "^1.1",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.2",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"satesh/phpcs-gitlab-report": "^1.0",
"squizlabs/php_codesniffer": "^3.3"
},
"suggest": {
"monolog/monolog": "Allows to log interaction with the API"
},
"type": "library",
"autoload": {
"files": [
"src/polyfill.php"
],
"psr-4": {
"Stancer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Stancer PHP library",
"homepage": "https://stancer.com",
"keywords": [
"card",
"payment solution",
"payments",
"sepa"
],
"support": {
"docs": "https://stancer.com/documentation/api/",
"issues": "https://gitlab.com/wearestancer/library/lib-php/-/issues",
"source": "https://gitlab.com/wearestancer/library/lib-php"
},
"time": "2024-11-15T17:47:59+00:00"
},
{
"name": "symfony/asset",
"version": "v7.3.0",

View File

@@ -0,0 +1,37 @@
<?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 Version20250729124444 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer_advert_payment ADD create_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL');
$this->addSql('ALTER TABLE customer_advert_payment ADD state VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE customer_advert_payment ADD payment_id VARCHAR(255) DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN customer_advert_payment.create_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_advert_payment DROP create_at');
$this->addSql('ALTER TABLE customer_advert_payment DROP state');
$this->addSql('ALTER TABLE customer_advert_payment DROP payment_id');
}
}

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 Version20250729124520 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer ADD stancer_id VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer DROP stancer_id');
}
}

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 Version20250729130827 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer_advert_payment ADD num_avis VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_advert_payment DROP num_avis');
}
}

View File

@@ -0,0 +1,35 @@
<?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 Version20250729134447 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 customer_advert_payment_line (id SERIAL NOT NULL, advert_payment_id INT DEFAULT NULL, pos INT NOT NULL, name VARCHAR(255) NOT NULL, content TEXT NOT NULL, price_ht VARCHAR(255) NOT NULL, tva DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_CE5C43D1DEFD281 ON customer_advert_payment_line (advert_payment_id)');
$this->addSql('ALTER TABLE customer_advert_payment_line ADD CONSTRAINT FK_CE5C43D1DEFD281 FOREIGN KEY (advert_payment_id) REFERENCES customer_advert_payment (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 customer_advert_payment_line DROP CONSTRAINT FK_CE5C43D1DEFD281');
$this->addSql('DROP TABLE customer_advert_payment_line');
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Controller\Artemis\Intranet;
use App\Entity\Customer;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentLine;
use App\Entity\CustomerContact;
use App\Entity\CustomerDevis;
use App\Entity\CustomerDevisLine;
@@ -15,6 +16,7 @@ use App\Repository\CustomerDnsRepository;
use App\Repository\CustomerRepository;
use App\Service\Customer\Billing\CreateDevisCustomerEvent;
use App\Service\Customer\Billing\CreateDevisCustomerEventSend;
use App\Service\Customer\CreateAvisEvent;
use App\Service\Customer\DeleteCustomerEvent;
use App\Service\Customer\RestoreCustomerEvent;
use App\Service\Logger\LoggerService;
@@ -92,6 +94,35 @@ class CustomerController extends AbstractController
}
if($request->query->has('idDevis') && $request->query->has('act')) {
if($request->query->get('act') == "createAvisPayment") {
/** @var CustomerDevis $devis */
$devis = $entityManager->getRepository(CustomerDevis::class)->find($request->query->get('idDevis'));
$avisPayment = new CustomerAdvertPayment();
$t = new \DateTimeImmutable();
$num = "A-".$t->format('Y/m')."/".sprintf('%05d',$entityManager->getRepository(CustomerAdvertPayment::class)->count()+1);
$avisPayment->setNumAvis($num);
$avisPayment->setCustomer($customer);
$avisPayment->setDevis($devis);
$avisPayment->setCreateAt(new \DateTimeImmutable());
$avisPayment->setState("created");
foreach ($devis->getCustomerDevisLines() as $line) {
$avisLine = new CustomerAdvertPaymentLine();
$avisLine->setName($line->getName());
$avisLine->setTva($line->getTva());
$avisLine->setPos($line->getPos());
$avisLine->setContent($line->getContent());
$avisLine->setPriceHt($line->getPriceHT());
$entityManager->persist($avisLine);
$avisPayment->addCustomerAdvertPaymentLine($avisLine);
$entityManager->persist($avisPayment);
}
$entityManager->persist($avisPayment);
$entityManager->flush();
}
$event = new CreateAvisEvent($entityManager->getRepository(CustomerAdvertPayment::class)->findAll()[0]);
$eventDispatcher->dispatch($event);
if($request->query->get('act') == "cancelDevis") {
/** @var CustomerDevis $devis */
$devis = $entityManager->getRepository(CustomerDevis::class)->find($request->query->get('idDevis'));
@@ -214,6 +245,9 @@ class CustomerController extends AbstractController
{
/** @var CustomerDevis $devis */
$devis = $entityManager->getRepository(CustomerDevis::class)->find($request->get('idDevis'));
if($devis->getState() == "cancel" ||$devis->getState() == "accepted")
return $this->redirectToRoute('artemis_intranet_customer_view',['id'=>$customer->getId(),'current'=>'order']);
$devis->setEvent($eventDispatcher);
if($request->isMethod('POST')) {
$data = $_POST;
$devis->setCreateAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i',$data['date']));

View File

@@ -98,6 +98,9 @@ class Customer
#[ORM\OneToMany(targetEntity: CustomerOrder::class, mappedBy: 'customer')]
private Collection $customerOrders;
#[ORM\Column(length: 255, nullable: true)]
private ?string $stancerId = null;
public function __construct()
{
$this->customerContacts = new ArrayCollection();
@@ -472,4 +475,16 @@ class Customer
return $this;
}
public function getStancerId(): ?string
{
return $this->stancerId;
}
public function setStancerId(?string $stancerId): static
{
$this->stancerId = $stancerId;
return $this;
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Entity;
use App\Repository\CustomerAdvertPaymentRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomerAdvertPaymentRepository::class)]
@@ -22,6 +24,29 @@ class CustomerAdvertPayment
#[ORM\OneToOne(mappedBy: 'avisPayment', cascade: ['persist', 'remove'])]
private ?CustomerOrder $customerOrder = null;
#[ORM\Column]
private ?\DateTimeImmutable $createAt = null;
#[ORM\Column(length: 255)]
private ?string $state = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $paymentId = null;
#[ORM\Column(length: 255)]
private ?string $numAvis = null;
/**
* @var Collection<int, CustomerAdvertPaymentLine>
*/
#[ORM\OneToMany(targetEntity: CustomerAdvertPaymentLine::class, mappedBy: 'advertPayment')]
private Collection $customerAdvertPaymentLines;
public function __construct()
{
$this->customerAdvertPaymentLines = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@@ -72,4 +97,82 @@ class CustomerAdvertPayment
return $this;
}
public function getCreateAt(): ?\DateTimeImmutable
{
return $this->createAt;
}
public function setCreateAt(\DateTimeImmutable $createAt): static
{
$this->createAt = $createAt;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getPaymentId(): ?string
{
return $this->paymentId;
}
public function setPaymentId(?string $paymentId): static
{
$this->paymentId = $paymentId;
return $this;
}
public function getNumAvis(): ?string
{
return $this->numAvis;
}
public function setNumAvis(string $numAvis): static
{
$this->numAvis = $numAvis;
return $this;
}
/**
* @return Collection<int, CustomerAdvertPaymentLine>
*/
public function getCustomerAdvertPaymentLines(): Collection
{
return $this->customerAdvertPaymentLines;
}
public function addCustomerAdvertPaymentLine(CustomerAdvertPaymentLine $customerAdvertPaymentLine): static
{
if (!$this->customerAdvertPaymentLines->contains($customerAdvertPaymentLine)) {
$this->customerAdvertPaymentLines->add($customerAdvertPaymentLine);
$customerAdvertPaymentLine->setAdvertPayment($this);
}
return $this;
}
public function removeCustomerAdvertPaymentLine(CustomerAdvertPaymentLine $customerAdvertPaymentLine): static
{
if ($this->customerAdvertPaymentLines->removeElement($customerAdvertPaymentLine)) {
// set the owning side to null (unless already changed)
if ($customerAdvertPaymentLine->getAdvertPayment() === $this) {
$customerAdvertPaymentLine->setAdvertPayment(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Entity;
use App\Repository\CustomerAdvertPaymentLineRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomerAdvertPaymentLineRepository::class)]
class CustomerAdvertPaymentLine
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'customerAdvertPaymentLines')]
private ?CustomerAdvertPayment $advertPayment = null;
#[ORM\Column]
private ?int $pos = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\Column(length: 255)]
private ?string $priceHt = null;
#[ORM\Column]
private ?float $tva = null;
public function getId(): ?int
{
return $this->id;
}
public function getAdvertPayment(): ?CustomerAdvertPayment
{
return $this->advertPayment;
}
public function setAdvertPayment(?CustomerAdvertPayment $advertPayment): static
{
$this->advertPayment = $advertPayment;
return $this;
}
public function getPos(): ?int
{
return $this->pos;
}
public function setPos(int $pos): static
{
$this->pos = $pos;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
public function getPriceHt(): ?string
{
return $this->priceHt;
}
public function setPriceHt(string $priceHt): static
{
$this->priceHt = $priceHt;
return $this;
}
public function getTva(): ?float
{
return $this->tva;
}
public function setTva(float $tva): static
{
$this->tva = $tva;
return $this;
}
}

View File

@@ -2,10 +2,13 @@
namespace App\Entity;
use App\Exception\ImmutableDevisFieldException;
use App\Exception\ImmutableLoggerFieldException;
use App\Repository\CustomerDevisRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
@@ -89,6 +92,13 @@ class CustomerDevis
private ?string $devisOriginalName = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
private EventDispatcherInterface $eventDispatcher;
public function setEvent(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function __construct()
{
@@ -150,6 +160,9 @@ class CustomerDevis
public function setNumDevis(string $numDevis): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->numDevis = $numDevis;
return $this;
@@ -162,6 +175,9 @@ class CustomerDevis
public function setCreateAt(\DateTimeImmutable $createAt): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->createAt = $createAt;
return $this;
@@ -174,6 +190,9 @@ class CustomerDevis
public function setDevisSubmiterId(?string $devisSubmiterId): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->devisSubmiterId = $devisSubmiterId;
return $this;
@@ -186,6 +205,9 @@ class CustomerDevis
public function setSignAt(?\DateTimeImmutable $signAt): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->signAt = $signAt;
return $this;
@@ -198,6 +220,9 @@ class CustomerDevis
public function setState(string $state): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->state = $state;
return $this;
@@ -223,6 +248,9 @@ class CustomerDevis
public function removeCustomerDevisLine(CustomerDevisLine $customerDevisLine): static
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
if ($this->customerDevisLines->removeElement($customerDevisLine)) {
// set the owning side to null (unless already changed)
if ($customerDevisLine->getDevis() === $this) {
@@ -265,6 +293,9 @@ class CustomerDevis
*/
public function setUpdateAt(?\DateTimeImmutable $updateAt): void
{
if ($this->state == "cancel" || $this->state == "accepted") {
$this->eventDispatcher->dispatch(new ImmutableDevisFieldException("numDevis"));
}
$this->updateAt = $updateAt;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Exception;
class ImmutableDevisFieldException extends \LogicException
{
public function __construct(string $field)
{
parent::__construct(sprintf("The '%s' field is immutable once set.", $field));
}
}

View File

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

View File

@@ -2,28 +2,82 @@
namespace App\Service\Customer;
use App\Entity\CustomerAdvertPaymentLine;
use App\Service\Customer\Billing\CreateDevisCustomerEvent;
use App\Service\Customer\Billing\CreateDevisCustomerEventSend;
use App\Service\Docuseal\SignClient;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\DevisPdf;
use App\Service\Pdf\PaymentPdf;
use App\Service\Stancer\Client;
use Doctrine\ORM\EntityManagerInterface;
use Stancer\Customer;
use Stancer\Payment;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Uid\Uuid;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AsEventListener(event: CreateDevisCustomerEvent::class, method: 'onBillingEvent')]
#[AsEventListener(event: CreateDevisCustomerEventSend::class, method: 'onBillingEventSend')]
#[AsEventListener(event: CreateAvisEvent::class, method: 'onCreatedAvisEvent')]
class BillingEventSusbriber
{
public function __construct(private readonly UploaderHelper $uploaderHelper,private readonly Mailer $mailer,private readonly SignClient $signClient,private readonly EntityManagerInterface $entityManager,private KernelInterface $kernel)
public function __construct(private readonly UrlGeneratorInterface $urlGenerator,private readonly Client $client,private readonly UploaderHelper $uploaderHelper,private readonly Mailer $mailer,private readonly SignClient $signClient,private readonly EntityManagerInterface $entityManager,private KernelInterface $kernel)
{
}
public function onCreatedAvisEvent(CreateAvisEvent $createAvisEvent): void
{
$createAvis = $createAvisEvent->getCustomerAdvertPayment();
$customer = $createAvis->getCustomer();
$customerId = $this->client->customer($customer);
$customer->setStancerId($customerId);
$this->entityManager->persist($customer);
$this->entityManager->flush();
if($createAvis->getPaymentId() == null) {
$paymentReturnPath = $this->urlGenerator->generate("app_login", [], UrlGeneratorInterface::ABSOLUTE_URL);
$paymentReturnPath = str_replace("http://", "https://", $paymentReturnPath);
$total = 0;
/** @var CustomerAdvertPaymentLine $item */
foreach ($createAvis->getCustomerAdvertPaymentLines() as $item) {
$total = $total + (floatval($item->getPriceHt()) * 1.20);
}
$customerStancer = new Customer($customerId);
//creeat payement link
$payment = new Payment();
$payment->setAmount($total * 100);
$payment->setCurrency("EUR");
$payment->setDescription("Paiement de l'avis de paiement - " . $createAvis->getNumAvis());
$payment->setCustomer($customerStancer);
$payment->setReturnUrl($paymentReturnPath);
$payment->setOrderId($createAvis->getNumAvis());
$payment->setMethodsAllowed(["card"]);
$payment->setCapture(true);
$paimentId = $data = $payment->send();
$createAvis->setPaymentId($paimentId);
$this->entityManager->persist($createAvis);
$this->entityManager->flush();
}
$payEdit = str_replace('"',"",$createAvis->getPaymentId());
$paymentItem = new Payment($payEdit);
$url = $paymentItem->getPaymentPageUrl();
$pdf = New PaymentPdf($this->kernel,$createAvis,$url);
$pdf->generate();
$pdf->Output('I');
//generate pdf
//insert qrcode paiement
// send mail customer
}
public function onBillingEventSend(CreateDevisCustomerEventSend $createDevisCustomerEventSend)
{
$devis = $createDevisCustomerEventSend->getCustomerDevis();

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Service\Customer;
use App\Entity\CustomerAdvertPayment;
class CreateAvisEvent
{
private CustomerAdvertPayment $customerAdvertPayment;
public function __construct(CustomerAdvertPayment $customerAdvertPayment)
{
$this->customerAdvertPayment = $customerAdvertPayment;
}
/**
* @return CustomerAdvertPayment
*/
public function getCustomerAdvertPayment(): CustomerAdvertPayment
{
return $this->customerAdvertPayment;
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Service\Pdf;
use App\Entity\CustomerAdvertPayment;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Uid\Uuid;
define('EURO_AVIS',chr(128));
class PaymentPdf extends Fpdf
{
private $items;
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()),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Avis de paiment N° ","ISO-8859-1","UTF-8").$this->customerDevis->getNumAvis());
}
function Header()
{
$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'));
$this->Text(30, 25, mb_convert_encoding("s.com@siteconseil.fr", 'ISO-8859-1', 'UTF-8'));
$this->Text(30, 30, mb_convert_encoding("03 23 05 62 43", 'ISO-8859-1', 'UTF-8'));
$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'));
}
if($this->customerDevis->getCustomer()->getAddress3() != "") {
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
}
$y=$y+5;
$this->Text(120,$y,mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode()." ".$this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$this->body();
}
function Footer()
{
$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');
$this->Ln(5);
$this->Cell(0, 5, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
public function body()
{
$this->Line(120,70,120,220);
$this->Line(160,70,160,220);
$this->Text(125,70,mb_convert_encoding("PRIX HT","ISO-8859-1","UTF-8"));
$this->Text(165,70,mb_convert_encoding("PRIX TTC","ISO-8859-1","UTF-8"));
}
function generate()
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
foreach($this->items as $item) {
if ($this->GetY() + 30 > $this->PageBreakTrigger) {
$this->AddPage();
$this->body();
}
$this->SetY($this->GetPageHeight() / 3.75);
$this->SetFont('Arial', 'B', 12);
$this->Cell(100,10,mb_convert_encoding($item['title'],'ISO-8859-1','UTF-8'),0,0);
$this->SetFont('Arial', '', 12);
$this->SetX($this->GetX()+12);
$this->Cell(35,10,number_format($item['priceHt'],2,",")." ".EURO_AVIS,0);
$this->SetX($this->GetX()+5);
$this->Cell(35,10,number_format($item['priceTTC'],2,",")." ".EURO_AVIS,0,true);
$this->SetX($this->GetX());
$this->MultiCell(100, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0,0,0);
}
$this->displaySummary();
}
function displaySummary()
{
$builder = new Builder(
writer: new PngWriter(),
writerOptions: [],
data: $this->urlPaiment,
size: 300,
margin: 0,
encoding: new Encoding('UTF-8'),
labelText: 'Payée ICI',
labelFont: new OpenSans(20),
labelAlignment: LabelAlignment::Center
);
$result = $builder->build();
$tmpname = Uuid::v4().".png";
$dir = sys_get_temp_dir().'/'.$tmpname;
$result->saveToFile($dir);
$this->Image($dir,25,230,40,40);
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20; // Assuming TVA is 20%
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60); // Adjust this value as needed
// Move to the left for signature and date tags
// Add signature and date tags
//$this->Text(10,$this->GetY(), mb_convert_encoding($signDateTag, 'ISO-8859-1', 'UTF-8'));
// $this->Text(10,$this->GetY()+10, mb_convert_encoding($signTag, 'ISO-8859-1', 'UTF-8'));
// Display the summary
$this->SetFont('Arial', 'B', 12);
$this->Cell(150, 10, mb_convert_encoding('Total HT:', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalHT, 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
$this->Cell(150, 10, mb_convert_encoding('TVA (20%):', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTVA, 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
$this->Cell(150, 10, mb_convert_encoding('Total TTC:', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(35, 10, number_format($totalTTC, 2, ",") . " " . EURO_AVIS, 0, 1, 'R');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Service\Stancer;
use Doctrine\ORM\EntityManagerInterface;
use Stancer\Config;
use Stancer\Customer;
class Client
{
private Config $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']);
}
public function customer(?\App\Entity\Customer $customer)
{
if($customer->getStancerId() == null) {
$customerItem = new Customer();
$customerItem->setEmail($customer->mainContact()->getEmail());
$customerItem->setName($customer->getRaisonSocial());
$result = $customerItem->send();
return json_decode($result, true);
}
return $customer->getStancerId();
}
}