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:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }} € HT</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-white/80 text-sm font-bold">{{ price.priceHt }} € HT</span>
|
||||
{% if price.monthPrice != '0.00' %}
|
||||
<span class="text-[#fabf04]">+ {{ price.monthPrice }} €/mois</span>
|
||||
<span class="text-[#fabf04] text-sm font-bold">+ {{ price.monthPrice }} €/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>
|
||||
|
||||
Reference in New Issue
Block a user