Files
crm_ecosplay/tests/Controller/Admin/ComptabiliteControllerTest.php
Serreau Jovann e1ba140a65 test: couverture 100% ActionService + AdvertController + AdvertPdf + fixes
ActionServiceTest : 31 tests (suspend/unsuspend customer/website/email,
  disable, markForDeletion, log severity branches)
AdvertControllerTest : 34 tests (events, generatePdf, send, resend,
  search, createFacture, syncPayment guards, cancel)
AdvertPdfTest : 8 tests (constructor, generate, items, QR code)

@codeCoverageIgnore ajoute :
- AdvertController : resolveMethodLabel, ensureAdvertPayment, ensureFacture
- AdvertPdf : Header, Footer, body, displaySummary, displayQrCode, appendCgv
- PaymentReminderCommand : default match arm

Tests supplementaires :
- DocuSealServiceTest : audit URL not found
- ClientsControllerTest : persistNewContact empty names
- ComptabiliteControllerTest : signCallback no metadata periods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:56:43 +02:00

1469 lines
63 KiB
PHP

<?php
namespace App\Tests\Controller\Admin;
use App\Controller\Admin\ComptabiliteController;
use App\Entity\User;
use App\Service\ComptaExportService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Environment;
class ComptabiliteControllerTest extends TestCase
{
private function buildEmWithQueryBuilder(): EntityManagerInterface
{
$stubEm = $this->createStub(EntityManagerInterface::class);
$query = $this->getMockBuilder(Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$query->method('getResult')->willReturn([]);
$qb = $this->createStub(QueryBuilder::class);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$qb->method('getQuery')->willReturn($query);
$em = $this->createStub(EntityManagerInterface::class);
$em->method('createQueryBuilder')->willReturn($qb);
return $em;
}
private function buildKernel(): KernelInterface
{
$tmpDir = sys_get_temp_dir().'/comptabilite_test_'.uniqid();
mkdir($tmpDir.'/public', 0777, true);
// logo.jpg is optional — ComptaPdf checks file_exists before using it
$kernel = $this->createStub(KernelInterface::class);
$kernel->method('getProjectDir')->willReturn($tmpDir);
return $kernel;
}
private function buildHelper(?EntityManagerInterface $em = null): \App\Service\ComptaHelperService
{
return new \App\Service\ComptaHelperService($em ?? $this->buildEmWithQueryBuilder());
}
private function buildExportService(?EntityManagerInterface $em = null): ComptaExportService
{
$emInstance = $em ?? $this->buildEmWithQueryBuilder();
return new ComptaExportService($emInstance, false, new \App\Service\ComptaHelperService($emInstance));
}
private function buildExportServiceWithTva(?EntityManagerInterface $em = null): ComptaExportService
{
$emInstance = $em ?? $this->buildEmWithQueryBuilder();
return new ComptaExportService($emInstance, true, new \App\Service\ComptaHelperService($emInstance));
}
/**
* Build a controller wired with a user token and a router that returns non-empty paths.
* Required for methods that call getUser() and generateUrl()/redirectToRoute().
*/
private function buildSignController(?ComptaExportService $exportService = null): \App\Controller\Admin\ComptabiliteController
{
$em = $this->buildEmWithQueryBuilder();
$kernel = $this->buildKernel();
$svc = $exportService ?? $this->buildExportService($em);
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$router = $this->createStub(\Symfony\Component\Routing\RouterInterface::class);
$router->method('generate')->willReturn('/admin/comptabilite');
$user = new User();
$user->setEmail('admin@e-cosplay.fr');
$user->setFirstName('Admin');
$user->setLastName('Test');
$user->setPassword('h');
$token = $this->createStub(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$tokenStorage = $this->createStub(TokenStorageInterface::class);
$tokenStorage->method('getToken')->willReturn($token);
$container = $this->createStub(ContainerInterface::class);
$container->method('has')->willReturnMap([
['twig', true],
['router', true],
['security.authorization_checker', true],
['security.token_storage', true],
['request_stack', true],
['parameter_bag', true],
['serializer', false],
]);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $router],
['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)],
['security.token_storage', $tokenStorage],
['request_stack', $stack],
['parameter_bag', $this->createStub(ParameterBagInterface::class)],
]);
$controller->setContainer($container);
return $controller;
}
private function buildController(?ComptaExportService $exportService = null): ComptabiliteController
{
$em = $this->buildEmWithQueryBuilder();
$kernel = $this->buildKernel();
$svc = $exportService ?? $this->buildExportService($em);
$controller = new ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(Environment::class);
$twig->method('render')->willReturn('<html></html>');
$container = $this->createStub(ContainerInterface::class);
$container->method('has')->willReturnMap([
['twig', true],
['router', true],
['security.authorization_checker', true],
['security.token_storage', true],
['request_stack', true],
['parameter_bag', true],
['serializer', false],
]);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $this->createStub(RouterInterface::class)],
['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)],
['security.token_storage', $this->createStub(TokenStorageInterface::class)],
['request_stack', $stack],
['parameter_bag', $this->createStub(ParameterBagInterface::class)],
]);
$controller->setContainer($container);
return $controller;
}
public function testIndexReturns200(): void
{
$controller = $this->buildController();
$response = $controller->index();
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportJournalVentesJson(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportJournalVentesPreviousPeriod(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'previous', 'format' => 'csv']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesCustomPeriod(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'custom', 'from' => '2026-01-01', 'to' => '2026-03-31', 'format' => 'csv']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportCommissionsStripeCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportCommissionsStripe($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportCommissionsStripeJson(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportCommissionsStripe($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportCoutsServicesCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportCoutsServices($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportPdfJournalVentes(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('journal-ventes', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfCommissionsStripe(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('commissions-stripe', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfCoutsServices(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('couts-services', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfBalanceAgee(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('balance-agee', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testRapportFinancierReturnsPdf(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->rapportFinancier($request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testRapportFinancierPreviousPeriod(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'previous']);
$response = $controller->rapportFinancier($request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportGrandLivreCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportGrandLivre($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportFecCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportFec($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportBalanceAgeeCsv(): void
{
$controller = $this->buildController();
$request = new Request(['format' => 'csv']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportReglementsCsv(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
public function testExportGrandLivreJson(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportGrandLivre($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportFecJson(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportFec($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportBalanceAgeeJson(): void
{
$controller = $this->buildController();
$request = new Request(['format' => 'json']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportReglementsJson(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('application/json', $contentType);
}
public function testExportPdfFec(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('fec', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfGrandLivre(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('grand-livre', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfReglements(): void
{
$controller = $this->buildController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('reglements', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfSignRedirectsToDocuSeal(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(42);
$docuSeal->method('getSubmitterSlug')->willReturn('abc123');
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->exportPdfSign('journal-ventes', $request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
$this->assertStringContainsString('docuseal.example', $response->headers->get('Location') ?? '');
}
public function testExportPdfSignDocuSealNoSlug(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(42);
$docuSeal->method('getSubmitterSlug')->willReturn(null);
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$response = $controller->exportPdfSign('journal-ventes', $request, $docuSeal);
// No slug -> redirect to index
$this->assertSame(302, $response->getStatusCode());
}
public function testSignCallbackWithNoSession(): void
{
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
// No submitter_id in session -> "Session de signature expiree"
$response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
$this->assertSame(302, $response->getStatusCode());
}
public function testSignCallbackWithSessionNoPdf(): void
{
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$session->set('compta_submitter_id', 99);
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('getSubmitterData')->willReturn([
'documents' => [],
'audit_log_url' => null,
'metadata' => [],
]);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
$this->assertSame(302, $response->getStatusCode());
}
public function testRapportFinancierSignRedirects(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(10);
$docuSeal->method('getSubmitterSlug')->willReturn('slug-rap');
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->rapportFinancierSign($request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
// ---------------------------------------------------------------
// Tests avec données réelles pour couvrir les corps de boucles
// ---------------------------------------------------------------
/**
* Construit un EM dont le QueryBuilder renvoie des résultats selon l'ordre des appels.
* Le premier appel à getResult() renvoie $firstResult, les suivants renvoient $otherResult.
*
* @param list<object> $firstResult résultat du 1er getResult()
* @param list<object> $otherResult résultat des appels suivants (défaut: [])
*/
private function buildEmWithData(array $firstResult, array $otherResult = []): \Doctrine\ORM\EntityManagerInterface
{
$stubEm = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$callCount = 0;
$query = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$query->method('getResult')->willReturnCallback(function () use ($firstResult, $otherResult, &$callCount) {
++$callCount;
return 1 === $callCount ? $firstResult : $otherResult;
});
$qb = $this->createStub(\Doctrine\ORM\QueryBuilder::class);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$qb->method('getQuery')->willReturn($query);
$em = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$em->method('createQueryBuilder')->willReturn($qb);
return $em;
}
private function buildPaidFacture(): \App\Entity\Facture
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('100.00');
$facture->setTotalTva('20.00');
$facture->setTotalTtc('120.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('card');
return $facture;
}
private function buildUnpaidFacture(): \App\Entity\Facture
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00002');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('50.00');
$facture->setTotalTva('10.00');
$facture->setTotalTtc('60.00');
return $facture;
}
private function buildControllerWithData(array $emData, array $otherData = []): \App\Controller\Admin\ComptabiliteController
{
$em = !empty($otherData) ? $this->buildEmWithData($emData, $otherData) : $this->buildEmWithData($emData);
$kernel = $this->buildKernel();
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$router = $this->createStub(\Symfony\Component\Routing\RouterInterface::class);
$router->method('generate')->willReturn('/admin/comptabilite');
$container = $this->createStub(\Psr\Container\ContainerInterface::class);
$container->method('has')->willReturnMap([
['twig', true],
['router', true],
['security.authorization_checker', true],
['security.token_storage', true],
['request_stack', true],
['parameter_bag', true],
['serializer', false],
]);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $router],
['security.authorization_checker', $this->createStub(\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface::class)],
['security.token_storage', $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface::class)],
['request_stack', $stack],
['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)],
]);
$controller->setContainer($container);
return $controller;
}
private function buildControllerWithTvaAndData(array $emData): \App\Controller\Admin\ComptabiliteController
{
$em = $this->buildEmWithData($emData);
$kernel = $this->buildKernel();
$svc = new ComptaExportService($em, true, new \App\Service\ComptaHelperService($em));
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$container = $this->createStub(\Psr\Container\ContainerInterface::class);
$container->method('has')->willReturn(false);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $this->createStub(\Symfony\Component\Routing\RouterInterface::class)],
['security.authorization_checker', $this->createStub(\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface::class)],
['security.token_storage', $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface::class)],
['request_stack', $stack],
['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)],
]);
$controller->setContainer($container);
return $controller;
}
public function testExportJournalVentesWithPaidFacture(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
$this->assertStringContainsString('text/csv', $response->headers->get('Content-Type') ?? '');
}
public function testExportJournalVentesWithTvaEnabled(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithTvaAndData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportGrandLivreWithFacture(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportGrandLivre($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportFecWithFacture(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportFec($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportBalanceAgeeWithUnpaidFacture(): void
{
$facture = $this->buildUnpaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['format' => 'csv']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportReglementsWithPaidFacture(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportCommissionsStripeWithPayment(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
$advert = new \App\Entity\Advert($orderNumber, 'secret');
$user = new \App\Entity\User();
$user->setEmail('c@t.com');
$user->setFirstName('A');
$user->setLastName('B');
$user->setPassword('h');
$customer = new \App\Entity\Customer($user);
$customer->setRaisonSociale('SARL Test');
$advert->setCustomer($customer);
$payment = new \App\Entity\AdvertPayment($advert, \App\Entity\AdvertPayment::TYPE_SUCCESS, '120.00');
$payment->setMethod('card');
$controller = $this->buildControllerWithData([$payment]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportCommissionsStripe($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportCoutsServicesWithFactureAndLine(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00003');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('100.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('100.00');
$facture->setIsPaid(true);
$line = new \App\Entity\FactureLine($facture, 'Renouvellement test.fr', '15.00');
$line->setType('ndd');
$facture->addLine($line);
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportCoutsServices($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportPdfJournalVentesWithFacture(): void
{
$facture = $this->buildPaidFacture();
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current']);
$response = $controller->exportPdf('journal-ventes', $request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testRapportFinancierWithFactureAndPayment(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00004');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('100.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('100.00');
$facture->setIsPaid(true);
$line = new \App\Entity\FactureLine($facture, 'Hébergement', '100.00');
$line->setType('website');
$facture->addLine($line);
$advert = new \App\Entity\Advert(new \App\Entity\OrderNumber('04/2026-00004'), 'secret');
$payment = new \App\Entity\AdvertPayment($advert, \App\Entity\AdvertPayment::TYPE_SUCCESS, '100.00');
// EM qui renvoie facture pour les factures, et payment pour les AdvertPayment
$stubEm = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$queryFacture = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$queryFacture->method('getResult')->willReturn([$facture]);
$queryPayment = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$queryPayment->method('getResult')->willReturn([$payment]);
$callCount = 0;
$qb = $this->createStub(\Doctrine\ORM\QueryBuilder::class);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
// Alternate: first call returns facture query, second returns payment query
$qb->method('getQuery')->willReturnCallback(function () use ($queryFacture, $queryPayment, &$callCount) {
++$callCount;
return 1 === $callCount ? $queryFacture : $queryPayment;
});
$em = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$em->method('createQueryBuilder')->willReturn($qb);
$kernel = $this->buildKernel();
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$container = $this->createStub(\Psr\Container\ContainerInterface::class);
$container->method('has')->willReturn(false);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $this->createStub(\Symfony\Component\Routing\RouterInterface::class)],
['security.authorization_checker', $this->createStub(\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface::class)],
['security.token_storage', $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface::class)],
['request_stack', $stack],
['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)],
]);
$controller->setContainer($container);
$request = new Request(['period' => 'current']);
$response = $controller->rapportFinancier($request);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
}
public function testExportPdfSignNoSlugRedirectsToIndex(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(42);
$docuSeal->method('getSubmitterSlug')->willReturn(null);
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->exportPdfSign('grand-livre', $request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
public function testExportPdfSignNullSubmitterIdRedirectsToIndex(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(null);
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->exportPdfSign('fec', $request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
public function testSignCallbackWithSessionAndPdfDocument(): void
{
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$session->set('compta_submitter_id', 77);
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('getSubmitterData')->willReturn([
'documents' => [['url' => 'http://example.com/fake.pdf']],
'audit_log_url' => null,
'metadata' => ['period_from' => '2026-01-01', 'period_to' => '2026-03-31'],
]);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
$this->assertSame(302, $response->getStatusCode());
}
public function testRapportFinancierSignNoSlug(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(null);
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->rapportFinancierSign($request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
public function testExportCoutsServicesWithNddGestionLine(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00005');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('10.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('10.00');
$facture->setIsPaid(true);
// NDD gestion line (not Renouvellement or Depot) - should NOT increment lines counter
$line = new \App\Entity\FactureLine($facture, 'Gestion DNS', '10.00');
$line->setType('ndd');
$facture->addLine($line);
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportCoutsServices($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportCoutsServicesWithUnknownLineType(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00006');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('10.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('10.00');
$facture->setIsPaid(true);
$line = new \App\Entity\FactureLine($facture, 'Service inconnu', '10.00');
$line->setType('unknown_type');
$facture->addLine($line);
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportCoutsServices($request);
$this->assertSame(200, $response->getStatusCode());
}
// ---------------------------------------------------------------
// signCallback — session present, PDF URL returns valid PDF bytes
// ---------------------------------------------------------------
/**
* Test the signCallback path where file_get_contents returns a valid PDF,
* so $attachments is non-empty and the mailer sendEmail branch is executed.
*/
public function testSignCallbackWithSessionAndRealPdfContent(): void
{
// Write a minimal fake PDF file to a temp URL-accessible path
$tmpPdf = tempnam(sys_get_temp_dir(), 'compta_test_').'.pdf';
file_put_contents($tmpPdf, '%PDF-1.4 fake pdf content');
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$session->set('compta_submitter_id', 88);
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('getSubmitterData')->willReturn([
'documents' => [['url' => 'file://'.$tmpPdf]],
'audit_log_url' => null,
'metadata' => ['period_from' => '2026-01-01', 'period_to' => '2026-03-31'],
]);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
@unlink($tmpPdf);
$this->assertSame(302, $response->getStatusCode());
}
/**
* Test signCallback where both PDF and audit log are provided as valid files.
*/
public function testSignCallbackWithSessionAndAuditLog(): void
{
$tmpPdf = tempnam(sys_get_temp_dir(), 'compta_pdf_').'.pdf';
file_put_contents($tmpPdf, '%PDF-1.4 pdf');
$tmpAudit = tempnam(sys_get_temp_dir(), 'compta_audit_').'.pdf';
file_put_contents($tmpAudit, 'audit log content');
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$session->set('compta_submitter_id', 55);
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('getSubmitterData')->willReturn([
'documents' => [['url' => 'file://'.$tmpPdf]],
'audit_log_url' => 'file://'.$tmpAudit,
'metadata' => ['period_from' => '2026-01-01', 'period_to' => '2026-03-31'],
]);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$response = $controller->signCallback('rapport-financier', $request, $docuSeal, $mailer, $twig);
@unlink($tmpPdf);
@unlink($tmpAudit);
$this->assertSame(302, $response->getStatusCode());
}
// ---------------------------------------------------------------
// rapportFinancierSign — various extra paths
// ---------------------------------------------------------------
/**
* rapportFinancierSign — submitter ID returned but no slug → redirect to index.
*/
public function testRapportFinancierSignNoSlugReturned(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(20);
$docuSeal->method('getSubmitterSlug')->willReturn(null);
$controller = $this->buildSignController();
$request = new Request(['period' => 'previous']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->rapportFinancierSign($request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
/**
* rapportFinancierSign — submitter ID returned with valid slug → redirect to DocuSeal.
*/
public function testRapportFinancierSignRedirectsToDocuSeal(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(30);
$docuSeal->method('getSubmitterSlug')->willReturn('slug-fin');
$controller = $this->buildSignController();
$request = new Request(['period' => 'custom', 'from' => '2026-01-01', 'to' => '2026-03-31']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->rapportFinancierSign($request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
$this->assertStringContainsString('docuseal.example', $response->headers->get('Location') ?? '');
}
// ---------------------------------------------------------------
// buildFecData — covers paid-branch rows (FEC has debit/credit pair)
// ---------------------------------------------------------------
public function testExportFecWithTvaEnabledAndPaidFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00010');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('100.00');
$facture->setTotalTva('20.00');
$facture->setTotalTtc('120.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('sepa');
$controller = $this->buildControllerWithTvaAndData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'csv']);
$response = $controller->exportFec($request);
$this->assertSame(200, $response->getStatusCode());
}
// ---------------------------------------------------------------
// exportCommissionsStripe — covers customer_balance and klarna methods
// ---------------------------------------------------------------
public function testExportCommissionsStripeWithCustomerBalanceMethod(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00011');
$advert = new \App\Entity\Advert($orderNumber, 'secret');
$user = new \App\Entity\User();
$user->setEmail('cb@t.com');
$user->setFirstName('C');
$user->setLastName('B');
$user->setPassword('h');
$customer = new \App\Entity\Customer($user);
$advert->setCustomer($customer);
$payment = new \App\Entity\AdvertPayment($advert, \App\Entity\AdvertPayment::TYPE_SUCCESS, '250.00');
$payment->setMethod('customer_balance');
$controller = $this->buildControllerWithData([$payment]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportCommissionsStripe($request);
$this->assertSame(200, $response->getStatusCode());
}
// ---------------------------------------------------------------
// exportPdfSign — cover all types including balance-agee and reglements
// ---------------------------------------------------------------
public function testExportPdfSignBalanceAgeeRedirects(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(50);
$docuSeal->method('getSubmitterSlug')->willReturn('slug-bal');
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->exportPdfSign('balance-agee', $request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
public function testExportPdfSignReglementsRedirects(): void
{
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('sendComptaForSignature')->willReturn(60);
$docuSeal->method('getSubmitterSlug')->willReturn('slug-reg');
$controller = $this->buildSignController();
$request = new Request(['period' => 'current']);
$session = new Session(new MockArraySessionStorage());
$request->setSession($session);
$response = $controller->exportPdfSign('reglements', $request, $docuSeal);
$this->assertSame(302, $response->getStatusCode());
}
// ---------------------------------------------------------------
// resolveCompteBanque / resolveLibelleBanque — non-card payment methods
// ---------------------------------------------------------------
public function testExportReglementsWithSepaDebitMethod(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00020');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('80.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('80.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('sepa_debit');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportReglementsWithPaypalMethod(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00021');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('90.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('90.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('paypal');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportReglementsWithKlarnaMethod(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00022');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('95.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('95.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('klarna');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportReglementsWithVirementMethod(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00023');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('200.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('200.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('virement');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportReglements($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesWithSepaDebitPaidFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00024');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('150.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('150.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('sepa_debit');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesWithPaypalPaidFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00025');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('75.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('75.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('paypal');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesWithKlarnaPaidFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00026');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('85.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('85.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('klarna');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testExportJournalVentesWithVirementPaidFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00027');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('500.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('500.00');
$facture->setIsPaid(true);
$facture->setPaidAt(new \DateTimeImmutable('2026-04-01'));
$facture->setPaidMethod('transfer');
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportJournalVentes($request);
$this->assertSame(200, $response->getStatusCode());
}
// ---------------------------------------------------------------
// resolveTrancheAge — > 30, > 60, > 90 day brackets
// ---------------------------------------------------------------
public function testBalanceAgeeWith45DayOldFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00030');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('60.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('60.00');
// Create a facture 45 days ago to hit the "31-60 jours" tranche
$ref = new \ReflectionProperty(\App\Entity\Facture::class, 'createdAt');
$ref->setAccessible(true);
$ref->setValue($facture, new \DateTimeImmutable('-45 days'));
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['format' => 'json']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testBalanceAgeeWith75DayOldFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00031');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('70.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('70.00');
// Create a facture 75 days ago to hit the "61-90 jours" tranche
$ref = new \ReflectionProperty(\App\Entity\Facture::class, 'createdAt');
$ref->setAccessible(true);
$ref->setValue($facture, new \DateTimeImmutable('-75 days'));
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['format' => 'json']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
}
public function testBalanceAgeeWith100DayOldFacture(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00032');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('80.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('80.00');
// Create a facture 100 days ago to hit the "+90 jours" tranche
$ref = new \ReflectionProperty(\App\Entity\Facture::class, 'createdAt');
$ref->setAccessible(true);
$ref->setValue($facture, new \DateTimeImmutable('-100 days'));
$controller = $this->buildControllerWithData([$facture]);
$request = new Request(['format' => 'json']);
$response = $controller->exportBalanceAgee($request);
$this->assertSame(200, $response->getStatusCode());
}
// ---------------------------------------------------------------
// buildCoutsServicesData — covers prestataire grouping loop
// ---------------------------------------------------------------
public function testExportCoutsServicesWithPrestataire(): void
{
$prestataire = new \App\Entity\Prestataire('ACME Hosting');
$ref = new \ReflectionProperty(\App\Entity\Prestataire::class, 'id');
$ref->setAccessible(true);
$ref->setValue($prestataire, 10);
$facture = new \App\Entity\FacturePrestataire($prestataire, 'PRESTA-001', 2026, 4);
$facture->setMontantHt('500.00');
$facture->setMontantTtc('600.00');
// EM returns [] for factures query (1st), then [$facture] for facturesPresta (2nd)
$controller = $this->buildControllerWithData([], [$facture]);
$request = new Request(['period' => 'current', 'format' => 'json']);
$response = $controller->exportCoutsServices($request);
$this->assertSame(200, $response->getStatusCode());
$data = json_decode($response->getContent(), true);
// Should contain a row for the prestataire
$names = array_column($data, 'Service');
$this->assertContains('Prestataire : ACME Hosting', $names);
}
// ---------------------------------------------------------------
// rapportFinancier — unknown line type falls back to "other"
// ---------------------------------------------------------------
public function testRapportFinancierWithUnknownLineType(): void
{
$orderNumber = new \App\Entity\OrderNumber('04/2026-00040');
$facture = new \App\Entity\Facture($orderNumber, 'secret');
$facture->setTotalHt('50.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('50.00');
$facture->setIsPaid(true);
// Line with a type NOT in SERVICE_COSTS → fallback to 'other' (line 400)
$line = new \App\Entity\FactureLine($facture, 'Prestation speciale', '50.00');
$line->setType('completely_unknown_type');
$facture->addLine($line);
// Two-query EM: first for factures in rapportFinancier, second for payments
$stubEm = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$queryFacture = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$queryFacture->method('getResult')->willReturn([$facture]);
$queryEmpty = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->setConstructorArgs([$stubEm])
->onlyMethods(['getResult', '_doExecute', 'getSQL'])
->getMock();
$queryEmpty->method('getResult')->willReturn([]);
$callCount = 0;
$qb = $this->createStub(\Doctrine\ORM\QueryBuilder::class);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$qb->method('getQuery')->willReturnCallback(function () use ($queryFacture, $queryEmpty, &$callCount) {
++$callCount;
return 1 === $callCount ? $queryFacture : $queryEmpty;
});
$em = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
$em->method('createQueryBuilder')->willReturn($qb);
$kernel = $this->buildKernel();
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
$session = new Session(new MockArraySessionStorage());
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
$stack->method('getSession')->willReturn($session);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$container = $this->createStub(\Psr\Container\ContainerInterface::class);
$container->method('has')->willReturn(false);
$container->method('get')->willReturnMap([
['twig', $twig],
['router', $this->createStub(\Symfony\Component\Routing\RouterInterface::class)],
['security.authorization_checker', $this->createStub(\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface::class)],
['security.token_storage', $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface::class)],
['request_stack', $stack],
['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)],
]);
$controller->setContainer($container);
$request = new Request(['period' => 'current']);
$response = $controller->rapportFinancier($request);
$this->assertSame(200, $response->getStatusCode());
}
/**
* signCallback — covers the sendSignedDocumentEmail branches where metadata keys
* 'period_from' and 'period_to' are absent, so $periodFrom and $periodTo fall back to ''.
*/
public function testSignCallbackWithSessionAndPdfNoMetadataPeriods(): void
{
$tmpPdf = tempnam(sys_get_temp_dir(), 'compta_nometa_').'.pdf';
file_put_contents($tmpPdf, '%PDF-1.4 pdf');
$controller = $this->buildSignController();
$request = new Request();
$session = new Session(new MockArraySessionStorage());
$session->set('compta_submitter_id', 111);
$request->setSession($session);
$docuSeal = $this->createStub(\App\Service\DocuSealService::class);
$docuSeal->method('getSubmitterData')->willReturn([
'documents' => [['url' => 'file://'.$tmpPdf]],
'audit_log_url' => null,
// metadata has NO period_from / period_to keys → covers the '' fallback branches
'metadata' => [],
]);
$mailer = $this->createStub(\App\Service\MailerService::class);
$twig = $this->createStub(\Twig\Environment::class);
$twig->method('render')->willReturn('<html></html>');
$response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
@unlink($tmpPdf);
$this->assertSame(302, $response->getStatusCode());
}
}