feat: comptabilite + prestataires + rapport financier + stats dynamiques
Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@ class MembresControllerTest extends TestCase
|
||||
$controller = new MembresController($this->createStub(LoggerInterface::class));
|
||||
$controller->setContainer($this->createContainer());
|
||||
|
||||
$response = $controller->index($this->createKeycloak($users, [['name' => 'siteconseil_member']]), $userRepo);
|
||||
$response = $controller->index($this->createKeycloak($users, [['name' => 'ecosplay_member']]), $userRepo);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class MembresControllerTest extends TestCase
|
||||
public function testIndexWithCreatedGroups(): void
|
||||
{
|
||||
$kc = $this->createStub(KeycloakAdminService::class);
|
||||
$kc->method('ensureRequiredGroups')->willReturn(['siteconseil_member', 'siteconseil_admin']);
|
||||
$kc->method('ensureRequiredGroups')->willReturn(['ecosplay_member', 'ecosplay_admin']);
|
||||
$kc->method('listUsers')->willReturn([]);
|
||||
$kc->method('listGroups')->willReturn([]);
|
||||
|
||||
@@ -209,7 +209,7 @@ class MembresControllerTest extends TestCase
|
||||
$twig = $this->createStub(Environment::class);
|
||||
$twig->method('render')->willReturn('<html>email</html>');
|
||||
|
||||
$request = new Request([], ['firstName' => 'Jean', 'lastName' => 'Membre', 'email' => 'jean@test.com', 'groups' => ['siteconseil_member', 'esy-web']]);
|
||||
$request = new Request([], ['firstName' => 'Jean', 'lastName' => 'Membre', 'email' => 'jean@test.com', 'groups' => ['ecosplay_member', 'esy-web']]);
|
||||
$request->setMethod('POST');
|
||||
|
||||
$controller = new MembresController($this->createStub(LoggerInterface::class));
|
||||
@@ -230,7 +230,7 @@ class MembresControllerTest extends TestCase
|
||||
$hasher = $this->createStub(UserPasswordHasherInterface::class);
|
||||
$hasher->method('hashPassword')->willReturn('hashed');
|
||||
|
||||
$request = new Request([], ['firstName' => 'Admin', 'lastName' => 'User', 'email' => 'admin@test.com', 'groups' => ['siteconseil_admin']]);
|
||||
$request = new Request([], ['firstName' => 'Admin', 'lastName' => 'User', 'email' => 'admin@test.com', 'groups' => ['ecosplay_admin']]);
|
||||
$request->setMethod('POST');
|
||||
|
||||
$controller = new MembresController($this->createStub(LoggerInterface::class));
|
||||
|
||||
@@ -225,7 +225,7 @@ class SyncControllerTest extends TestCase
|
||||
$controller = new SyncController();
|
||||
$controller->setContainer($this->createContainer());
|
||||
|
||||
$response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.siteconseil.fr');
|
||||
$response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.e-cosplay.fr');
|
||||
$this->assertSame(302, $response->getStatusCode());
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ class SyncControllerTest extends TestCase
|
||||
$controller = new SyncController();
|
||||
$controller->setContainer($this->createContainer());
|
||||
|
||||
$response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.siteconseil.fr/');
|
||||
$response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.e-cosplay.fr/');
|
||||
$this->assertSame(302, $response->getStatusCode());
|
||||
$this->assertSame('whsec_new', $existing->getSecret());
|
||||
$this->assertSame('we_new', $existing->getEndpointId());
|
||||
|
||||
@@ -23,7 +23,7 @@ class CspReportControllerTest extends TestCase
|
||||
$stack->method('getSession')->willReturn($session);
|
||||
|
||||
$paramBag = $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class);
|
||||
$paramBag->method('get')->willReturn('admin@siteconseil.fr');
|
||||
$paramBag->method('get')->willReturn('admin@e-cosplay.fr');
|
||||
$paramBag->method('has')->willReturn(true);
|
||||
|
||||
$container = $this->createStub(ContainerInterface::class);
|
||||
@@ -73,7 +73,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => 'chrome-extension://abc123',
|
||||
'blocked-uri' => 'inline',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -91,7 +91,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => 'moz-extension://abc',
|
||||
'blocked-uri' => '',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -109,7 +109,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => 'http://localhost:3000/app.js',
|
||||
'blocked-uri' => 'eval',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -143,9 +143,9 @@ class CspReportControllerTest extends TestCase
|
||||
|
||||
$payload = json_encode([
|
||||
'csp-report' => [
|
||||
'source-file' => 'https://crm.siteconseil.fr/app.js',
|
||||
'source-file' => 'https://crm.e-cosplay.fr/app.js',
|
||||
'blocked-uri' => 'wasm-eval',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -163,7 +163,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => '',
|
||||
'blocked-uri' => 'about:blank',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'frame-src',
|
||||
],
|
||||
]);
|
||||
@@ -183,7 +183,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => 'https://evil.com/inject.js',
|
||||
'blocked-uri' => 'https://evil.com',
|
||||
'document-uri' => 'https://crm.siteconseil.fr/dashboard',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr/dashboard',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -205,7 +205,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => 'https://evil.com/inject.js',
|
||||
'blocked-uri' => 'https://evil.com',
|
||||
'document-uri' => 'https://crm.siteconseil.fr/dashboard',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr/dashboard',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
@@ -224,7 +224,7 @@ class CspReportControllerTest extends TestCase
|
||||
$payload = json_encode([
|
||||
'source-file' => 'https://evil.com/inject.js',
|
||||
'blocked-uri' => 'https://evil.com',
|
||||
'document-uri' => 'https://crm.siteconseil.fr/dashboard',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr/dashboard',
|
||||
'violated-directive' => 'script-src',
|
||||
]);
|
||||
$request = new Request([], [], [], [], [], [], $payload);
|
||||
@@ -241,7 +241,7 @@ class CspReportControllerTest extends TestCase
|
||||
'csp-report' => [
|
||||
'source-file' => '/home/user/node_modules/some-lib/index.js',
|
||||
'blocked-uri' => 'inline',
|
||||
'document-uri' => 'https://crm.siteconseil.fr',
|
||||
'document-uri' => 'https://crm.e-cosplay.fr',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -18,7 +18,7 @@ class DomainEmailTest extends TestCase
|
||||
$user->setLastName('T');
|
||||
$user->setPassword('h');
|
||||
|
||||
return new Domain(new Customer($user), 'siteconseil.fr');
|
||||
return new Domain(new Customer($user), 'e-cosplay.fr');
|
||||
}
|
||||
|
||||
public function testConstructor(): void
|
||||
@@ -29,7 +29,7 @@ class DomainEmailTest extends TestCase
|
||||
$this->assertNull($email->getId());
|
||||
$this->assertSame($domain, $email->getDomain());
|
||||
$this->assertSame('contact', $email->getName());
|
||||
$this->assertSame('contact@siteconseil.fr', $email->getFullEmail());
|
||||
$this->assertSame('contact@e-cosplay.fr', $email->getFullEmail());
|
||||
$this->assertSame(DomainEmail::STATE_ACTIVE, $email->getState());
|
||||
$this->assertTrue($email->isActive());
|
||||
$this->assertSame(5120, $email->getQuotaMb());
|
||||
@@ -43,7 +43,7 @@ class DomainEmailTest extends TestCase
|
||||
|
||||
$email->setName(' Support ');
|
||||
$this->assertSame('support', $email->getName());
|
||||
$this->assertSame('support@siteconseil.fr', $email->getFullEmail());
|
||||
$this->assertSame('support@e-cosplay.fr', $email->getFullEmail());
|
||||
|
||||
$email->setQuotaMb(10240);
|
||||
$this->assertSame(10240, $email->getQuotaMb());
|
||||
|
||||
@@ -40,7 +40,7 @@ class DomainTest extends TestCase
|
||||
|
||||
public function testSetters(): void
|
||||
{
|
||||
$domain = new Domain($this->createCustomer(), 'siteconseil.fr');
|
||||
$domain = new Domain($this->createCustomer(), 'e-cosplay.fr');
|
||||
|
||||
$domain->setFqdn(' ESY-WEB.DEV ');
|
||||
$this->assertSame('esy-web.dev', $domain->getFqdn());
|
||||
|
||||
@@ -33,7 +33,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testWebmailRedirect(): void
|
||||
{
|
||||
$event = $this->createEvent('webmail.siteconseil.fr');
|
||||
$event = $this->createEvent('webmail.e-cosplay.fr');
|
||||
($this->createListener())($event);
|
||||
|
||||
$this->assertInstanceOf(RedirectResponse::class, $event->getResponse());
|
||||
@@ -42,7 +42,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testStatusRedirect(): void
|
||||
{
|
||||
$event = $this->createEvent('status.siteconseil.fr');
|
||||
$event = $this->createEvent('status.e-cosplay.fr');
|
||||
($this->createListener())($event);
|
||||
|
||||
$this->assertInstanceOf(RedirectResponse::class, $event->getResponse());
|
||||
@@ -51,7 +51,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testNoRedirectOnCrmHost(): void
|
||||
{
|
||||
$event = $this->createEvent('crm.siteconseil.fr');
|
||||
$event = $this->createEvent('crm.e-cosplay.fr');
|
||||
($this->createListener())($event);
|
||||
|
||||
$this->assertNull($event->getResponse());
|
||||
@@ -59,7 +59,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testNoRedirectWhenAlreadyOnTarget(): void
|
||||
{
|
||||
$event = $this->createEvent('webmail.siteconseil.fr', '/webmail');
|
||||
$event = $this->createEvent('webmail.e-cosplay.fr', '/webmail');
|
||||
($this->createListener())($event);
|
||||
|
||||
$this->assertNull($event->getResponse());
|
||||
@@ -67,7 +67,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testNoRedirectOnSubPath(): void
|
||||
{
|
||||
$event = $this->createEvent('status.siteconseil.fr', '/status/api');
|
||||
$event = $this->createEvent('status.e-cosplay.fr', '/status/api');
|
||||
($this->createListener())($event);
|
||||
|
||||
$this->assertNull($event->getResponse());
|
||||
@@ -75,7 +75,7 @@ class SubdomainRedirectListenerTest extends TestCase
|
||||
|
||||
public function testSubRequestIgnored(): void
|
||||
{
|
||||
$request = Request::create('https://webmail.siteconseil.fr/');
|
||||
$request = Request::create('https://webmail.e-cosplay.fr/');
|
||||
$kernel = $this->createStub(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class KeycloakAuthenticatorTest extends TestCase
|
||||
'email' => 'test@example.com',
|
||||
'given_name' => 'John',
|
||||
'family_name' => 'Doe',
|
||||
'groups' => ['siteconseil_admin'],
|
||||
'groups' => ['superadmin'],
|
||||
]);
|
||||
|
||||
$client->method('fetchUserFromToken')->willReturn($keycloakUser);
|
||||
|
||||
@@ -36,7 +36,7 @@ class TwoFactorCodeMailerTest extends TestCase
|
||||
$this->mailer->expects($this->once())
|
||||
->method('send')
|
||||
->with($this->callback(function (Email $email) {
|
||||
return $email->getFrom()[0]->getAddress() === 'contact@siteconseil.fr'
|
||||
return $email->getFrom()[0]->getAddress() === 'contact@e-cosplay.fr'
|
||||
&& $email->getTo()[0]->getAddress() === 'test@example.com'
|
||||
&& $email->getSubject() === 'CRM SITECONSEIL - Code de verification'
|
||||
&& $email->getHtmlBody() === '<html>123456</html>';
|
||||
|
||||
@@ -150,11 +150,11 @@ class KeycloakAdminServiceTest extends TestCase
|
||||
|
||||
public function testListGroups(): void
|
||||
{
|
||||
$resp = $this->setupMocks('GET', '/groups', 200, [['id' => 'g1', 'name' => 'siteconseil_admin'], ['id' => 'g2', 'name' => 'esy-web']]);
|
||||
$resp = $this->setupMocks('GET', '/groups', 200, [['id' => 'g1', 'name' => 'ecosplay_admin'], ['id' => 'g2', 'name' => 'esy-web']]);
|
||||
$this->setupCallback([['method' => 'GET', 'url' => '/groups', 'response' => $resp]]);
|
||||
$groups = $this->service->listGroups();
|
||||
$this->assertCount(2, $groups);
|
||||
$this->assertSame('siteconseil_admin', $groups[0]['name']);
|
||||
$this->assertSame('ecosplay_admin', $groups[0]['name']);
|
||||
}
|
||||
|
||||
public function testCreateGroup(): void
|
||||
@@ -174,8 +174,8 @@ class KeycloakAdminServiceTest extends TestCase
|
||||
public function testGetRequiredGroups(): void
|
||||
{
|
||||
$groups = KeycloakAdminService::getRequiredGroups();
|
||||
$this->assertContains('siteconseil_admin', $groups);
|
||||
$this->assertContains('siteconseil_member', $groups);
|
||||
$this->assertContains('ecosplay_admin', $groups);
|
||||
$this->assertContains('ecosplay_member', $groups);
|
||||
$this->assertContains('esy-web', $groups);
|
||||
$this->assertContains('esy-mail', $groups);
|
||||
$this->assertCount(15, $groups);
|
||||
@@ -193,7 +193,7 @@ class KeycloakAdminServiceTest extends TestCase
|
||||
|
||||
public function testEnsureRequiredGroupsCreatesMissing(): void
|
||||
{
|
||||
$listResp = $this->setupMocks('GET', '/groups', 200, [['id' => 'g1', 'name' => 'siteconseil_admin']]);
|
||||
$listResp = $this->setupMocks('GET', '/groups', 200, [['id' => 'g1', 'name' => 'ecosplay_admin']]);
|
||||
$createResp = $this->setupMocks('POST', '/groups', 201);
|
||||
$this->setupCallback([
|
||||
['method' => 'GET', 'url' => '/groups', 'response' => $listResp],
|
||||
@@ -202,7 +202,7 @@ class KeycloakAdminServiceTest extends TestCase
|
||||
|
||||
$created = $this->service->ensureRequiredGroups();
|
||||
$this->assertNotEmpty($created);
|
||||
$this->assertNotContains('siteconseil_admin', $created);
|
||||
$this->assertNotContains('ecosplay_admin', $created);
|
||||
}
|
||||
|
||||
public function testTokenCaching(): void
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('app.js DOMContentLoaded', () => {
|
||||
describe('Member/Admin checkboxes', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<input type="checkbox" name="groups[]" value="siteconseil_member" checked>
|
||||
<input type="checkbox" name="groups[]" value="siteconseil_admin">
|
||||
<input type="checkbox" name="groups[]" value="gp_member" checked>
|
||||
<input type="checkbox" name="groups[]" value="superadmin">
|
||||
<input type="checkbox" name="groups[]" value="esy-web">
|
||||
<input type="checkbox" name="groups[]" value="esy-mail">
|
||||
`
|
||||
@@ -26,8 +26,8 @@ describe('app.js DOMContentLoaded', () => {
|
||||
it('unchecks other groups when member is checked', async () => {
|
||||
await loadApp()
|
||||
|
||||
const member = document.querySelector('[value="siteconseil_member"]')
|
||||
const admin = document.querySelector('[value="siteconseil_admin"]')
|
||||
const member = document.querySelector('[value="gp_member"]')
|
||||
const admin = document.querySelector('[value="superadmin"]')
|
||||
const esyWeb = document.querySelector('[value="esy-web"]')
|
||||
|
||||
admin.checked = true
|
||||
@@ -43,8 +43,8 @@ describe('app.js DOMContentLoaded', () => {
|
||||
it('checks all groups and unchecks member when admin is checked', async () => {
|
||||
await loadApp()
|
||||
|
||||
const member = document.querySelector('[value="siteconseil_member"]')
|
||||
const admin = document.querySelector('[value="siteconseil_admin"]')
|
||||
const member = document.querySelector('[value="gp_member"]')
|
||||
const admin = document.querySelector('[value="superadmin"]')
|
||||
const esyWeb = document.querySelector('[value="esy-web"]')
|
||||
|
||||
admin.checked = true
|
||||
@@ -57,8 +57,8 @@ describe('app.js DOMContentLoaded', () => {
|
||||
it('does nothing when admin is unchecked', async () => {
|
||||
await loadApp()
|
||||
|
||||
const member = document.querySelector('[value="siteconseil_member"]')
|
||||
const admin = document.querySelector('[value="siteconseil_admin"]')
|
||||
const member = document.querySelector('[value="gp_member"]')
|
||||
const admin = document.querySelector('[value="superadmin"]')
|
||||
|
||||
member.checked = true
|
||||
admin.checked = false
|
||||
|
||||
Reference in New Issue
Block a user