feat: index Meilisearch price_auto + bouton sync tarifs + statuts Stripe

src/Service/MeilisearchService.php:
- indexPrice(): indexe un PriceAutomatic dans l'index price_auto
- removePrice(): supprime un tarif de l'index
- searchPrices(): recherche dans les tarifs
- setupIndexes(): ajout de l'index price_auto avec searchableAttributes
  (type, title, description) et filterableAttributes (type, period)
- serializePrice(): serialise id, type, title, description, priceHt,
  monthPrice, period, stripeId, stripeAbonnementId

src/Service/TarificationService.php:
- Injection optionnelle de MeilisearchService dans le constructeur
- ensureDefaultPrices(): apres flush des nouveaux tarifs, les indexe
  automatiquement dans Meilisearch

src/Controller/Admin/SyncController.php:
- Injection de PriceAutomaticRepository dans index() pour compter les tarifs
- Nouvelle route POST /admin/sync/prices: synchronise tous les tarifs
  dans l'index price_auto de Meilisearch
- syncAll(): inclut maintenant les tarifs dans la synchronisation complete

templates/admin/sync/index.html.twig:
- Nouveau bloc "Tarifs" avec index price_auto, compteur en base,
  bouton "Synchroniser" vert

templates/admin/tarification/index.html.twig:
- Header de chaque tarif: badges de statut Stripe
  - "Stripe OK" (vert) si stripeId renseigne, "Non sync" (rouge) sinon
  - "Abo OK" (vert) si stripeAbonnementId renseigne, "Abo non sync"
    (rouge) sinon (affiche uniquement si monthPrice != 0.00)
- Champ Stripe Price ID abonnement masque si monthPrice == 0.00
  (pas d'abonnement pour les paiements uniques)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-02 22:45:52 +02:00
parent 32aa5b0d78
commit 49d4cb702d
5 changed files with 157 additions and 8 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Controller\Admin;
use App\Repository\CustomerRepository;
use App\Repository\PriceAutomaticRepository;
use App\Repository\RevendeurRepository;
use App\Service\MeilisearchService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -15,11 +16,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class SyncController extends AbstractController
{
#[Route('', name: 'index')]
public function index(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository): Response
public function index(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository, PriceAutomaticRepository $priceRepository): Response
{
return $this->render('admin/sync/index.html.twig', [
'totalCustomers' => $customerRepository->count([]),
'totalRevendeurs' => $revendeurRepository->count([]),
'totalPrices' => $priceRepository->count([]),
]);
}
@@ -57,8 +59,25 @@ class SyncController extends AbstractController
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/prices', name: 'prices', methods: ['POST'])]
public function syncPrices(PriceAutomaticRepository $priceRepository, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$prices = $priceRepository->findAll();
foreach ($prices as $price) {
$meilisearch->indexPrice($price);
}
$this->addFlash('success', \count($prices).' tarif(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync tarifs : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/all', name: 'all', methods: ['POST'])]
public function syncAll(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository, MeilisearchService $meilisearch): Response
public function syncAll(CustomerRepository $customerRepository, RevendeurRepository $revendeurRepository, PriceAutomaticRepository $priceRepository, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
@@ -73,7 +92,12 @@ class SyncController extends AbstractController
$meilisearch->indexRevendeur($revendeur);
}
$this->addFlash('success', \count($customers).' client(s) et '.\count($revendeurs).' revendeur(s) synchronise(s).');
$prices = $priceRepository->findAll();
foreach ($prices as $price) {
$meilisearch->indexPrice($price);
}
$this->addFlash('success', \count($customers).' client(s), '.\count($revendeurs).' revendeur(s) et '.\count($prices).' tarif(s) synchronise(s).');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync : '.$e->getMessage());
}

View File

@@ -3,6 +3,7 @@
namespace App\Service;
use App\Entity\Customer;
use App\Entity\PriceAutomatic;
use App\Entity\Revendeur;
use Meilisearch\Client;
use Psr\Log\LoggerInterface;
@@ -92,6 +93,42 @@ class MeilisearchService
}
}
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 setupIndexes(): void
{
try {
@@ -117,6 +154,39 @@ class MeilisearchService
$this->client->index('reseller')->updateFilterableAttributes([
'isActive',
]);
try {
$this->client->createIndex('price_auto', ['primaryKey' => 'id']);
} catch (\Throwable $e) {
$this->logger->warning('Meilisearch: setupIndexes (price_auto) - '.$e->getMessage());
}
$this->client->index('price_auto')->updateSearchableAttributes([
'type', 'title', 'description',
]);
$this->client->index('price_auto')->updateFilterableAttributes([
'type', 'period',
]);
}
/**
* @return array<string, mixed>
*/
/**
* @return array<string, mixed>
*/
private function serializePrice(PriceAutomatic $price): array
{
return [
'id' => $price->getId(),
'type' => $price->getType(),
'title' => $price->getTitle(),
'description' => $price->getDescription(),
'priceHt' => $price->getPriceHt(),
'monthPrice' => $price->getMonthPrice(),
'period' => $price->getPeriod(),
'stripeId' => $price->getStripeId(),
'stripeAbonnementId' => $price->getStripeAbonnementId(),
];
}
/**

View File

@@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
class TarificationService
{
private ?MeilisearchService $meilisearch = null;
private const DEFAULT_PRICES = [
'esyweb_business' => [
'title' => 'Esy-Web Business',
@@ -126,7 +127,9 @@ class TarificationService
public function __construct(
private PriceAutomaticRepository $repository,
private EntityManagerInterface $em,
?MeilisearchService $meilisearch = null,
) {
$this->meilisearch = $meilisearch;
}
/**
@@ -157,6 +160,21 @@ class TarificationService
if ([] !== $created) {
$this->em->flush();
if (null !== $this->meilisearch) {
foreach ($existing as $p) {
if (\in_array($p->getType(), $created, true)) {
$this->meilisearch->indexPrice($p);
}
}
// Les nouveaux sont deja flush, on les recharge
$allPrices = $this->repository->findAll();
foreach ($allPrices as $p) {
if (\in_array($p->getType(), $created, true)) {
$this->meilisearch->indexPrice($p);
}
}
}
}
return $created;

View File

@@ -73,6 +73,27 @@
</div>
</div>
{# Sync tarifs #}
<div class="glass p-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div>
<h2 class="font-bold uppercase text-sm">Tarifs</h2>
<p class="text-xs text-gray-500">Index Meilisearch : <strong>price_auto</strong></p>
<p class="text-xs text-gray-400 mt-0.5">{{ totalPrices }} tarif(s) en base</p>
</div>
</div>
<form method="post" action="{{ path('app_admin_sync_prices') }}">
<button type="submit" class="px-4 py-2 btn-glass text-green-600 font-bold uppercase text-[10px] tracking-wider">
Synchroniser
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -18,14 +18,26 @@
{% for price in prices %}
<div class="glass overflow-hidden">
<div class="glass-dark px-4 py-3 flex items-center justify-between" style="border-radius: 0;">
<div>
<div class="flex items-center gap-3">
<span class="text-white font-bold text-sm">{{ price.title }}</span>
<span class="ml-2 px-2 py-0.5 bg-white/10 text-white/60 text-[9px] font-bold uppercase rounded">{{ price.type }}</span>
<span class="px-2 py-0.5 bg-white/10 text-white/60 text-[9px] font-bold uppercase rounded">{{ price.type }}</span>
</div>
<div class="flex items-center gap-3 text-white/80 text-sm font-bold">
<span>{{ price.priceHt }} &#8364; HT</span>
<div class="flex items-center gap-3">
<span class="text-white/80 text-sm font-bold">{{ price.priceHt }} &#8364; HT</span>
{% if price.monthPrice != '0.00' %}
<span class="text-[#fabf04]">+ {{ price.monthPrice }} &#8364;/mois</span>
<span class="text-[#fabf04] text-sm font-bold">+ {{ price.monthPrice }} &#8364;/mois</span>
{% endif %}
{% if price.stripeId %}
<span class="px-2 py-0.5 bg-green-500/30 text-green-300 text-[9px] font-bold uppercase rounded">Stripe OK</span>
{% else %}
<span class="px-2 py-0.5 bg-red-500/30 text-red-300 text-[9px] font-bold uppercase rounded">Non sync</span>
{% endif %}
{% if price.monthPrice != '0.00' %}
{% if price.stripeAbonnementId %}
<span class="px-2 py-0.5 bg-green-500/30 text-green-300 text-[9px] font-bold uppercase rounded">Abo OK</span>
{% else %}
<span class="px-2 py-0.5 bg-red-500/30 text-red-300 text-[9px] font-bold uppercase rounded">Abo non sync</span>
{% endif %}
{% endif %}
</div>
</div>
@@ -55,10 +67,14 @@
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Stripe Price ID (unique)</label>
<input type="text" name="stripeId" value="{{ price.stripeId }}" placeholder="price_xxx" class="input-glass w-full px-3 py-2 text-sm font-medium font-mono">
</div>
{% if price.monthPrice != '0.00' %}
<div>
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Stripe Price ID (abonnement)</label>
<input type="text" name="stripeAbonnementId" value="{{ price.stripeAbonnementId }}" placeholder="price_xxx" class="input-glass w-full px-3 py-2 text-sm font-medium font-mono">
</div>
{% else %}
<input type="hidden" name="stripeAbonnementId" value="">
{% endif %}
<div class="md:col-span-2 lg:col-span-3">
<label class="block text-xs font-bold uppercase tracking-wider mb-1 text-gray-600">Description</label>
<textarea name="description" rows="2" class="input-glass w-full px-3 py-2 text-sm font-medium">{{ price.description }}</textarea>