Entites completes a 100% : - AdvertTest : 12 nouveaux (state, customer, totals, hmac, lines, payments) - CustomerTest : 3 nouveaux (isPendingDelete, revendeurCode, updatedAt) - DevisTest : 6 nouveaux (customer, submissionId, lines, state constants) - FactureTest : 10 nouveaux (state, totals, isPaid, lines, hmac, splitIndex) - OrderNumberTest : 1 nouveau (markAsUnused) - WebsiteTest : 1 nouveau (revendeurCode) Services completes/ameliores : - DocuSealServiceTest : 30 nouveaux (sendDevis, resendDevis, download, compta) - AdvertServiceTest : 6 nouveaux (isTvaEnabled, getTvaRate, computeTotals) - DevisServiceTest : 6 nouveaux (idem) - FactureServiceTest : 8 nouveaux (idem + createPaidFactureFromAdvert) - MailerServiceTest : 7 nouveaux (unsubscribe headers, VCF, formatFileSize) - MeilisearchServiceTest : 42 nouveaux (index/remove/search tous types) - RgpdServiceTest : 6 nouveaux (sendVerificationCode, verifyCode) - OrderNumberServiceTest : 2 nouveaux (preview/generate unused) - TarificationServiceTest : 1 nouveau (stripe error logger) - ComptaPdfTest : 4 nouveaux (totaux, colonnes numeriques, signature) - FacturePdfTest : 6 nouveaux (QR code, RIB, CGV Twig, footer skip) Controllers ameliores : - ComptabiliteControllerTest : 13 nouveaux (JSON, PDF, sign, callback) - StatsControllerTest : 2 nouveaux (rich data, 6-month evolution) - SyncControllerTest : 13 nouveaux (sync 6 types + purge) - ClientsControllerTest : 7 nouveaux (show, delete, resendWelcome) - FactureControllerTest : 2 nouveaux (generatePdf 404, send success) - LegalControllerTest : 6 nouveaux (rgpdVerify GET/POST) - TarificationControllerTest : 3 nouveaux (purge paths) - AdminControllersTest : 9 nouveaux (dashboard search, services) - WebhookStripeControllerTest : 3 nouveaux (invalid signatures) - KeycloakAuthenticatorTest : 4 nouveaux (groups, domain check) Commands : - PaymentReminderCommandTest : 1 nouveau (formalNotice step) - TestMailCommandTest : 2 nouveaux (force-dsn success/failure) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
344 lines
12 KiB
PHP
344 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Service\Pdf;
|
|
|
|
use App\Entity\Facture;
|
|
use App\Entity\FactureLine;
|
|
use App\Entity\OrderNumber;
|
|
use App\Service\Pdf\FacturePdf;
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\HttpKernel\KernelInterface;
|
|
|
|
class FacturePdfTest extends TestCase
|
|
{
|
|
private const HMAC_SECRET = 'test-hmac-secret';
|
|
|
|
private KernelInterface $kernel;
|
|
private string $projectDir;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->projectDir = sys_get_temp_dir().'/facture-pdf-test-'.bin2hex(random_bytes(4));
|
|
mkdir($this->projectDir.'/public', 0775, true);
|
|
|
|
$this->kernel = $this->createStub(KernelInterface::class);
|
|
$this->kernel->method('getProjectDir')->willReturn($this->projectDir);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->removeDir($this->projectDir);
|
|
}
|
|
|
|
private function removeDir(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
foreach (scandir($dir) as $item) {
|
|
if ('.' === $item || '..' === $item) {
|
|
continue;
|
|
}
|
|
$path = $dir.'/'.$item;
|
|
is_dir($path) ? $this->removeDir($path) : unlink($path);
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
|
|
private function makeFacture(string $numOrder = '04/2026-00001'): Facture
|
|
{
|
|
$orderNumber = new OrderNumber($numOrder);
|
|
|
|
return new Facture($orderNumber, self::HMAC_SECRET);
|
|
}
|
|
|
|
private function makeCustomer(bool $withRaisonSociale = false, bool $withAddress2 = false): \App\Entity\Customer
|
|
{
|
|
$customer = $this->createStub(\App\Entity\Customer::class);
|
|
$customer->method('getFullName')->willReturn('Jean Dupont');
|
|
$customer->method('getRaisonSociale')->willReturn($withRaisonSociale ? 'ACME SARL' : null);
|
|
$customer->method('getEmail')->willReturn('jean.dupont@example.com');
|
|
$customer->method('getAddress')->willReturn('42 rue des Tests');
|
|
$customer->method('getAddress2')->willReturn($withAddress2 ? 'Batiment B, etage 3' : null);
|
|
$customer->method('getZipCode')->willReturn('75001');
|
|
$customer->method('getCity')->willReturn('Paris');
|
|
|
|
return $customer;
|
|
}
|
|
|
|
public function testGenerateEmptyFactureProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithLinesProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setTotalHt('100.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('100.00');
|
|
|
|
$line1 = new FactureLine($facture, 'Hebergement Web', '60.00', 1);
|
|
$line1->setDescription('Hebergement annuel mutualisé');
|
|
$line2 = new FactureLine($facture, 'Nom de domaine', '40.00', 2);
|
|
$line2->setDescription('Renouvellement .fr annuel');
|
|
|
|
$facture->getLines()->add($line1);
|
|
$facture->getLines()->add($line2);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
$this->assertGreaterThan(1000, \strlen($output));
|
|
}
|
|
|
|
public function testGenerateWithCustomerAddressProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setCustomer($this->makeCustomer(false, false));
|
|
$facture->setTotalHt('50.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('50.00');
|
|
|
|
$line = new FactureLine($facture, 'Service test', '50.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithTvaProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setTotalHt('100.00');
|
|
$facture->setTotalTva('20.00');
|
|
$facture->setTotalTtc('120.00');
|
|
|
|
$line = new FactureLine($facture, 'Prestation avec TVA', '100.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithPaidFactureProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setTotalHt('200.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('200.00');
|
|
$facture->setIsPaid(true);
|
|
$facture->setPaidAt(new \DateTimeImmutable('2026-02-15'));
|
|
$facture->setPaidMethod('Virement');
|
|
|
|
$line = new FactureLine($facture, 'Service payé', '200.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithLogoFileProducesValidPdf(): void
|
|
{
|
|
// Minimal valid 1x1 JPEG
|
|
$jpegData = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AJQAB/9k=');
|
|
file_put_contents($this->projectDir.'/public/logo.jpg', $jpegData);
|
|
|
|
$facture = $this->makeFacture();
|
|
$line = new FactureLine($facture, 'Test', '10.00', 1);
|
|
$facture->getLines()->add($line);
|
|
$facture->setTotalHt('10.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('10.00');
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithManyLinesSpansMultiplePages(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setTotalHt('1500.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('1500.00');
|
|
|
|
for ($i = 1; $i <= 15; ++$i) {
|
|
$line = new FactureLine($facture, 'Service '.$i, '100.00', $i);
|
|
$line->setDescription('Description detaillee pour le service numero '.$i.' avec quelques mots supplementaires.');
|
|
$facture->getLines()->add($line);
|
|
}
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithCustomerRaisonSocialeProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture();
|
|
$facture->setCustomer($this->makeCustomer(true, true));
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithSplitIndexProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture('04/2026-00002');
|
|
$facture->setSplitIndex(2);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithQrCodeProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture('04/2026-00010');
|
|
$facture->setTotalHt('50.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('50.00');
|
|
|
|
$urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class);
|
|
$urlGenerator->method('generate')->willReturn('https://crm.e-cosplay.fr/facture/verify/1/abc123');
|
|
|
|
$line = new FactureLine($facture, 'Service QR', '50.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture, $urlGenerator);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithRibFileProducesValidPdf(): void
|
|
{
|
|
// Create a minimal valid PDF as the RIB file
|
|
$ribDir = $this->projectDir . '/public';
|
|
if (!is_dir($ribDir)) {
|
|
mkdir($ribDir, 0775, true);
|
|
}
|
|
|
|
// Create a minimal PDF file for RIB using FPDI itself
|
|
$miniPdf = new \setasign\Fpdi\Fpdi();
|
|
$miniPdf->AddPage();
|
|
$ribPath = $ribDir . '/rib.pdf';
|
|
$miniPdf->Output('F', $ribPath);
|
|
|
|
$facture = $this->makeFacture('04/2026-00011');
|
|
$facture->setTotalHt('75.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('75.00');
|
|
|
|
$line = new FactureLine($facture, 'Service RIB', '75.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithTwigAppendsCgvProducesValidPdf(): void
|
|
{
|
|
$facture = $this->makeFacture('04/2026-00012');
|
|
$facture->setTotalHt('80.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('80.00');
|
|
|
|
$line = new FactureLine($facture, 'Service Twig', '80.00', 1);
|
|
$facture->getLines()->add($line);
|
|
|
|
// Create a mock Twig environment that returns minimal HTML for CGV
|
|
$twig = $this->createStub(\Twig\Environment::class);
|
|
$twig->method('render')->willReturn('<html><body><p>Conditions Generales de Vente</p></body></html>');
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture, null, $twig);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testGenerateWithCustomerNoAddress(): void
|
|
{
|
|
$facture = $this->makeFacture('04/2026-00013');
|
|
|
|
$customer = $this->createStub(\App\Entity\Customer::class);
|
|
$customer->method('getFullName')->willReturn('Jean Dupont');
|
|
$customer->method('getRaisonSociale')->willReturn(null);
|
|
$customer->method('getEmail')->willReturn('jean@example.com');
|
|
$customer->method('getAddress')->willReturn(null); // No address
|
|
$customer->method('getAddress2')->willReturn(null);
|
|
$customer->method('getZipCode')->willReturn(null);
|
|
$customer->method('getCity')->willReturn(null);
|
|
$facture->setCustomer($customer);
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
|
|
public function testFooterOnCgvPageSkipsFooter(): void
|
|
{
|
|
// Test that skipHeaderFooter=true makes Footer skip on pages after the last facture page
|
|
$facture = $this->makeFacture('04/2026-00014');
|
|
$facture->setTotalHt('30.00');
|
|
$facture->setTotalTva('0.00');
|
|
$facture->setTotalTtc('30.00');
|
|
|
|
$twig = $this->createStub(\Twig\Environment::class);
|
|
$twig->method('render')->willReturn('<html><body><p>CGV page 1</p></body></html>');
|
|
|
|
$pdf = new FacturePdf($this->kernel, $facture, null, $twig);
|
|
$pdf->generate();
|
|
|
|
$output = $pdf->Output('S');
|
|
$this->assertStringStartsWith('%PDF', $output);
|
|
}
|
|
}
|