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>
1469 lines
63 KiB
PHP
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());
|
|
}
|
|
}
|