fix: SonarQube - CGV partial + MeilisearchService deduplique

- templates/pdf/_services_list.html.twig : liste services partagee
  entre pdf/cgv.html.twig et legal/cgv.html.twig
- MeilisearchService : extraction addToIndex/removeFromIndex/searchIndex
  generiques, serializeOrderDocument pour devis/advert/facture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-08 15:17:36 +02:00
parent cbe02f2ff5
commit e9e9acb130
5 changed files with 105 additions and 353 deletions

View File

@@ -17,7 +17,7 @@ sonar.php.coverage.reportPaths=var/reports/coverage.xml
sonar.php.tests.reportPath=var/reports/phpunit.xml
# Duplication exclusions
sonar.cpd.exclusions=migrations/**,src/Service/TarificationService.php,src/Entity/**,src/Repository/**,src/Service/Pdf/**,src/Service/AdvertService.php,src/Service/FactureService.php,src/Service/DevisService.php,templates/pdf/**
sonar.cpd.exclusions=migrations/**,src/Service/TarificationService.php,src/Entity/**,src/Repository/**,src/Service/Pdf/**,src/Service/AdvertService.php,src/Service/FactureService.php,src/Service/DevisService.php
# Global rule ignores
sonar.issue.ignore.multicriteria=e1,e2,e3

View File

@@ -21,6 +21,46 @@ class MeilisearchService
private Client $client;
/**
* @param array<string, mixed> $document
*/
private function addToIndex(string $index, array $document, string $label): void
{
try {
$this->client->index($index)->addDocuments([$document]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index '.$label.': '.$e->getMessage());
}
}
private function removeFromIndex(string $index, int $id, string $label): void
{
try {
$this->client->index($index)->deleteDocument($id);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove '.$label.' '.$id.': '.$e->getMessage());
}
}
/**
* @return list<array<string, mixed>>
*/
private function searchIndex(string $index, string $query, int $limit, ?int $customerId = null): array
{
try {
$options = ['limit' => $limit];
if (null !== $customerId) {
$options['filter'] = self::FILTER_CUSTOMER_ID.$customerId;
}
return $this->client->index($index)->search($query, $options)->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search '.$index.' error: '.$e->getMessage());
return [];
}
}
public function __construct(
private LoggerInterface $logger,
#[Autowire(env: 'MEILISEARCH_URL')] string $url,
@@ -29,314 +69,50 @@ class MeilisearchService
$this->client = new Client($url, $apiKey);
}
public function indexCustomer(Customer $customer): void
{
try {
$this->client->index('customer')->addDocuments([
$this->serializeCustomer($customer),
]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index customer '.$customer->getId().': '.$e->getMessage());
}
}
public function removeCustomer(int $customerId): void
{
try {
$this->client->index('customer')->deleteDocument($customerId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove customer '.$customerId.': '.$e->getMessage());
}
}
public function indexRevendeur(Revendeur $revendeur): void
{
try {
$this->client->index('reseller')->addDocuments([
$this->serializeRevendeur($revendeur),
]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index reseller '.$revendeur->getId().': '.$e->getMessage());
}
}
public function removeRevendeur(int $revendeurId): void
{
try {
$this->client->index('reseller')->deleteDocument($revendeurId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove reseller '.$revendeurId.': '.$e->getMessage());
}
}
/**
* @return list<array<string, mixed>>
*/
public function searchCustomers(string $query, int $limit = 20): array
{
try {
$results = $this->client->index('customer')->search($query, ['limit' => $limit]);
return $results->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Search customers failed for "'.$query.'": '.$e->getMessage());
return [];
}
}
/**
* @return list<array<string, mixed>>
*/
public function searchRevendeurs(string $query, int $limit = 20): array
{
try {
$results = $this->client->index('reseller')->search($query, ['limit' => $limit]);
return $results->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Search resellers failed for "'.$query.'": '.$e->getMessage());
return [];
}
}
public function indexPrice(PriceAutomatic $price): void
{
try {
$this->client->index('price_auto')->addDocuments([
$this->serializePrice($price),
]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index price '.$price->getId().': '.$e->getMessage());
}
}
public function removePrice(int $priceId): void
{
try {
$this->client->index('price_auto')->deleteDocument($priceId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove price '.$priceId.': '.$e->getMessage());
}
}
/**
* @return list<array<string, mixed>>
*/
public function searchPrices(string $query, int $limit = 20): array
{
try {
$results = $this->client->index('price_auto')->search($query, ['limit' => $limit]);
return $results->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Search prices failed for "'.$query.'": '.$e->getMessage());
return [];
}
}
public function indexContact(CustomerContact $contact): void
{
try {
$this->client->index('customer_contact')->addDocuments([
$this->serializeContact($contact),
]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index contact '.$contact->getId().': '.$e->getMessage());
}
}
public function removeContact(int $contactId): void
{
try {
$this->client->index('customer_contact')->deleteDocument($contactId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove contact '.$contactId.': '.$e->getMessage());
}
}
/**
* @return list<array<string, mixed>>
*/
public function searchContacts(string $query, int $limit = 20): array
{
try {
$results = $this->client->index('customer_contact')->search($query, ['limit' => $limit]);
return $results->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search contacts error: '.$e->getMessage());
return [];
}
}
public function indexDomain(Domain $domain): void
{
try {
$this->client->index('customer_ndd')->addDocuments([$this->serializeDomain($domain)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index domain '.$domain->getId().': '.$e->getMessage());
}
}
public function removeDomain(int $domainId): void
{
try {
$this->client->index('customer_ndd')->deleteDocument($domainId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove domain '.$domainId.': '.$e->getMessage());
}
}
public function indexCustomer(Customer $customer): void { $this->addToIndex('customer', $this->serializeCustomer($customer), 'customer '.$customer->getId()); }
public function removeCustomer(int $id): void { $this->removeFromIndex('customer', $id, 'customer'); }
/** @return list<array<string, mixed>> */
public function searchDomains(string $query, int $limit = 20): array
{
try {
return $this->client->index('customer_ndd')->search($query, ['limit' => $limit])->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search domains error: '.$e->getMessage());
return [];
}
}
public function indexWebsite(Website $website): void
{
try {
$this->client->index('customer_website')->addDocuments([$this->serializeWebsite($website)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index website '.$website->getId().': '.$e->getMessage());
}
}
public function removeWebsite(int $websiteId): void
{
try {
$this->client->index('customer_website')->deleteDocument($websiteId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove website '.$websiteId.': '.$e->getMessage());
}
}
public function searchCustomers(string $query, int $limit = 20): array { return $this->searchIndex('customer', $query, $limit); }
public function indexRevendeur(Revendeur $revendeur): void { $this->addToIndex('reseller', $this->serializeRevendeur($revendeur), 'reseller '.$revendeur->getId()); }
public function removeRevendeur(int $id): void { $this->removeFromIndex('reseller', $id, 'reseller'); }
/** @return list<array<string, mixed>> */
public function searchWebsites(string $query, int $limit = 20): array
{
try {
return $this->client->index('customer_website')->search($query, ['limit' => $limit])->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search websites error: '.$e->getMessage());
return [];
}
}
public function indexDevis(Devis $devis): void
{
try {
$this->client->index('customer_devis')->addDocuments([$this->serializeDevis($devis)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index devis '.$devis->getId().': '.$e->getMessage());
}
}
public function removeDevis(int $devisId): void
{
try {
$this->client->index('customer_devis')->deleteDocument($devisId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove devis '.$devisId.': '.$e->getMessage());
}
}
public function searchRevendeurs(string $query, int $limit = 20): array { return $this->searchIndex('reseller', $query, $limit); }
public function indexPrice(PriceAutomatic $price): void { $this->addToIndex('price_auto', $this->serializePrice($price), 'price '.$price->getId()); }
public function removePrice(int $id): void { $this->removeFromIndex('price_auto', $id, 'price'); }
/** @return list<array<string, mixed>> */
public function searchDevis(string $query, int $limit = 20, ?int $customerId = null): array
{
try {
$options = ['limit' => $limit];
if (null !== $customerId) {
$options['filter'] = self::FILTER_CUSTOMER_ID.$customerId;
}
return $this->client->index('customer_devis')->search($query, $options)->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search devis error: '.$e->getMessage());
return [];
}
}
public function indexAdvert(Advert $advert): void
{
try {
$this->client->index('customer_advert')->addDocuments([$this->serializeAdvert($advert)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index advert '.$advert->getId().': '.$e->getMessage());
}
}
public function removeAdvert(int $advertId): void
{
try {
$this->client->index('customer_advert')->deleteDocument($advertId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove advert '.$advertId.': '.$e->getMessage());
}
}
public function searchPrices(string $query, int $limit = 20): array { return $this->searchIndex('price_auto', $query, $limit); }
public function indexContact(CustomerContact $contact): void { $this->addToIndex('customer_contact', $this->serializeContact($contact), 'contact '.$contact->getId()); }
public function removeContact(int $id): void { $this->removeFromIndex('customer_contact', $id, 'contact'); }
/** @return list<array<string, mixed>> */
public function searchAdverts(string $query, int $limit = 20, ?int $customerId = null): array
{
try {
$options = ['limit' => $limit];
if (null !== $customerId) {
$options['filter'] = self::FILTER_CUSTOMER_ID.$customerId;
}
return $this->client->index('customer_advert')->search($query, $options)->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search adverts error: '.$e->getMessage());
return [];
}
}
public function indexFacture(Facture $facture): void
{
try {
$this->client->index('customer_facture')->addDocuments([$this->serializeFacture($facture)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index facture '.$facture->getId().': '.$e->getMessage());
}
}
public function removeFacture(int $factureId): void
{
try {
$this->client->index('customer_facture')->deleteDocument($factureId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove facture '.$factureId.': '.$e->getMessage());
}
}
public function searchContacts(string $query, int $limit = 20): array { return $this->searchIndex('customer_contact', $query, $limit); }
public function indexDomain(Domain $domain): void { $this->addToIndex('customer_ndd', $this->serializeDomain($domain), 'domain '.$domain->getId()); }
public function removeDomain(int $id): void { $this->removeFromIndex('customer_ndd', $id, 'domain'); }
/** @return list<array<string, mixed>> */
public function searchFactures(string $query, int $limit = 20, ?int $customerId = null): array
{
try {
$options = ['limit' => $limit];
if (null !== $customerId) {
$options['filter'] = self::FILTER_CUSTOMER_ID.$customerId;
}
public function searchDomains(string $query, int $limit = 20): array { return $this->searchIndex('customer_ndd', $query, $limit); }
return $this->client->index('customer_facture')->search($query, $options)->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search factures error: '.$e->getMessage());
public function indexWebsite(Website $website): void { $this->addToIndex('customer_website', $this->serializeWebsite($website), 'website '.$website->getId()); }
public function removeWebsite(int $id): void { $this->removeFromIndex('customer_website', $id, 'website'); }
/** @return list<array<string, mixed>> */
public function searchWebsites(string $query, int $limit = 20): array { return $this->searchIndex('customer_website', $query, $limit); }
return [];
}
}
public function indexDevis(Devis $devis): void { $this->addToIndex('customer_devis', $this->serializeDevis($devis), 'devis '.$devis->getId()); }
public function removeDevis(int $id): void { $this->removeFromIndex('customer_devis', $id, 'devis'); }
/** @return list<array<string, mixed>> */
public function searchDevis(string $query, int $limit = 20, ?int $customerId = null): array { return $this->searchIndex('customer_devis', $query, $limit, $customerId); }
public function indexAdvert(Advert $advert): void { $this->addToIndex('customer_advert', $this->serializeAdvert($advert), 'advert '.$advert->getId()); }
public function removeAdvert(int $id): void { $this->removeFromIndex('customer_advert', $id, 'advert'); }
/** @return list<array<string, mixed>> */
public function searchAdverts(string $query, int $limit = 20, ?int $customerId = null): array { return $this->searchIndex('customer_advert', $query, $limit, $customerId); }
public function indexFacture(Facture $facture): void { $this->addToIndex('customer_facture', $this->serializeFacture($facture), 'facture '.$facture->getId()); }
public function removeFacture(int $id): void { $this->removeFromIndex('customer_facture', $id, 'facture'); }
/** @return list<array<string, mixed>> */
public function searchFactures(string $query, int $limit = 20, ?int $customerId = null): array { return $this->searchIndex('customer_facture', $query, $limit, $customerId); }
public function purgeAllIndexes(): void
{
@@ -590,59 +366,43 @@ class MeilisearchService
];
}
/** @return array<string, mixed> */
private function serializeDevis(Devis $devis): array
/**
* @return array<string, mixed>
*/
private function serializeOrderDocument(object $doc, ?Customer $customer): array
{
$customer = $devis->getCustomer();
return [
'id' => $devis->getId(),
'numOrder' => $devis->getOrderNumber()->getNumOrder(),
'state' => $devis->getState(),
'totalHt' => $devis->getTotalHt(),
'totalTtc' => $devis->getTotalTtc(),
'id' => $doc->getId(),
'numOrder' => $doc->getOrderNumber()->getNumOrder(),
'state' => $doc->getState(),
'totalHt' => $doc->getTotalHt(),
'totalTtc' => $doc->getTotalTtc(),
'customerId' => $customer?->getId(),
'customerName' => $customer?->getFullName(),
'customerEmail' => $customer?->getEmail(),
'createdAt' => $devis->getCreatedAt()->format('Y-m-d'),
'createdAt' => $doc->getCreatedAt()->format('Y-m-d'),
];
}
/** @return array<string, mixed> */
private function serializeDevis(Devis $devis): array
{
return $this->serializeOrderDocument($devis, $devis->getCustomer());
}
/** @return array<string, mixed> */
private function serializeFacture(Facture $facture): array
{
$customer = $facture->getCustomer();
return [
'id' => $facture->getId(),
return array_merge($this->serializeOrderDocument($facture, $facture->getCustomer()), [
'invoiceNumber' => $facture->getInvoiceNumber(),
'state' => $facture->getState(),
'totalHt' => $facture->getTotalHt(),
'totalTtc' => $facture->getTotalTtc(),
'isPaid' => $facture->isPaid(),
'paidMethod' => $facture->getPaidMethod(),
'customerId' => $customer?->getId(),
'customerName' => $customer?->getFullName(),
'customerEmail' => $customer?->getEmail(),
'createdAt' => $facture->getCreatedAt()->format('Y-m-d'),
];
]);
}
/** @return array<string, mixed> */
private function serializeAdvert(Advert $advert): array
{
$customer = $advert->getCustomer();
return [
'id' => $advert->getId(),
'numOrder' => $advert->getOrderNumber()->getNumOrder(),
'state' => $advert->getState(),
'totalHt' => $advert->getTotalHt(),
'totalTtc' => $advert->getTotalTtc(),
'customerId' => $customer?->getId(),
'customerName' => $customer?->getFullName(),
'customerEmail' => $customer?->getEmail(),
'createdAt' => $advert->getCreatedAt()->format('Y-m-d'),
];
return $this->serializeOrderDocument($advert, $advert->getCustomer());
}
}

View File

@@ -20,16 +20,7 @@
<h2 class="text-xl font-bold uppercase mb-2">Article 2 - Prestations</h2>
<p>La Association E-Cosplay propose les prestations suivantes, accessibles via la plateforme <strong>crm.e-cosplay.fr</strong> :</p>
<ul class="list-disc pl-6 mt-2">
<li><strong>E-Site</strong> : creation, hebergement et maintenance de sites E-Site via le CMS <strong>Esy-Web</strong>, solution developpee par l'association</li>
<li><strong>E-Mail</strong> : hebergement et gestion de messagerie professionnelle, utilisant l'infrastructure Amazon Simple Email Service (AWS SES)</li>
<li><strong>E-Mailer</strong> : creation et envoi de campagnes emailing</li>
<li><strong>E-Track</strong> : suivi et analyse de frequentation des sites E-Site</li>
<li><strong>E-Protect</strong> : protection et securisation des infrastructures et des sites E-Site</li>
<li><strong>E-Translate</strong> : traduction automatique et multilingue des contenus</li>
<li><strong>E-Sign</strong> : signature electronique de documents</li>
<li><strong>E-Calendar</strong> : prise de rendez-vous en ligne, propulsee par <strong>Cal.com</strong></li>
<li><strong>E-Chat</strong> : chat en ligne en direct sur votre site E-Site, propulsee par <strong>Chatwoot</strong></li>
<li><strong>Service de Nom de Domaine</strong> : depot, enregistrement, renouvellement et gestion de noms de domaine pour le compte du client</li>
{{ include('pdf/_services_list.html.twig') }}
</ul>
<p class="mt-2">L'ensemble des services est heberge sur l'infrastructure geree par la Association E-Cosplay, a l'exception d'<strong>E-Calendar</strong> (propulsee par Cal.com) et d'<strong>E-Chat</strong> (propulsee par Chatwoot) qui sont des solutions tierces, sauf disposition exceptionnelle.</p>
<p class="mt-2">Le detail des prestations, leurs specificites techniques et les options disponibles sont precises dans le contrat de service conclu entre le client et la société.</p>

View File

@@ -0,0 +1,10 @@
<li><strong>E-Site</strong> : creation, hebergement et maintenance de sites E-Site via le CMS <strong>Esy-Web</strong>, solution developpee par l'association</li>
<li><strong>E-Mail</strong> : hebergement et gestion de messagerie professionnelle, utilisant l'infrastructure Amazon Simple Email Service (AWS SES)</li>
<li><strong>E-Mailer</strong> : creation et envoi de campagnes emailing</li>
<li><strong>E-Track</strong> : suivi et analyse de frequentation des sites E-Site</li>
<li><strong>E-Protect</strong> : protection et securisation des infrastructures et des sites E-Site</li>
<li><strong>E-Translate</strong> : traduction automatique et multilingue des contenus</li>
<li><strong>E-Sign</strong> : signature electronique de documents</li>
<li><strong>E-Calendar</strong> : prise de rendez-vous en ligne, propulsee par <strong>Cal.com</strong></li>
<li><strong>E-Chat</strong> : chat en ligne en direct sur votre site E-Site, propulsee par <strong>Chatwoot</strong></li>
<li><strong>Service de Nom de Domaine</strong> : depot, enregistrement, renouvellement et gestion de noms de domaine pour le compte du client</li>

View File

@@ -32,16 +32,7 @@
<h2>Article 2 - Prestations</h2>
<p>L'Association E-Cosplay propose les prestations suivantes, accessibles via la plateforme <strong>crm.e-cosplay.fr</strong> :</p>
<ul>
<li><strong>E-Site</strong> : creation, hebergement et maintenance de sites E-Site via le CMS <strong>Esy-Web</strong>, solution developpee par l'association</li>
<li><strong>E-Mail</strong> : hebergement et gestion de messagerie professionnelle, utilisant l'infrastructure Amazon Simple Email Service (AWS SES)</li>
<li><strong>E-Mailer</strong> : creation et envoi de campagnes emailing</li>
<li><strong>E-Track</strong> : suivi et analyse de frequentation des sites E-Site</li>
<li><strong>E-Protect</strong> : protection et securisation des infrastructures et des sites E-Site</li>
<li><strong>E-Translate</strong> : traduction automatique et multilingue des contenus</li>
<li><strong>E-Sign</strong> : signature electronique de documents</li>
<li><strong>E-Calendar</strong> : prise de rendez-vous en ligne, propulsee par <strong>Cal.com</strong></li>
<li><strong>E-Chat</strong> : chat en ligne en direct sur votre site E-Site, propulsee par <strong>Chatwoot</strong></li>
<li><strong>Service de Nom de Domaine</strong> : depot, enregistrement, renouvellement et gestion de noms de domaine pour le compte du client</li>
{{ include('pdf/_services_list.html.twig') }}
</ul>
<p>L'ensemble des services est heberge sur l'infrastructure geree par l'Association E-Cosplay, a l'exception d'<strong>E-Calendar</strong> (propulsee par Cal.com) et d'<strong>E-Chat</strong> (propulsee par Chatwoot) qui sont des solutions tierces, sauf disposition exceptionnelle.</p>
<p>Le detail des prestations, leurs specificites techniques et les options disponibles sont precises dans le contrat de service conclu entre le client et la societe.</p>