Add LibreTranslate auto-translation, improve test coverage, fix code duplication
Translation system: - Add LibreTranslate container (dev + prod), CPU-only, no port exposed, FR/EN/ES/DE/IT - Create app:translate command: reads *.fr.yaml, translates incrementally, preserves placeholders - Makefile: make trans / make trans_prod (stops container after translation) - Ansible: start libretranslate -> translate -> stop during deploy - Prod container restart: "no" (only runs during deploy) - .gitignore: ignore generated *.en/es/de/it.yaml files - 11 tests for TranslateCommand (API unreachable, empty, incremental, obsolete keys, placeholders, fallback) Test coverage improvements: - OrderController: event ended (400), invalid cart JSON, invalid email, stock zero (4 new tests) - AccountController: finance stats all statuses (paid/pending/refunded/cancelled), soldCounts (2 new tests) - JS cart: checkout without error elements, hide error on retry, stock polling edge cases (singular, no label, qty zero, unknown billet) (8 new tests) - JS editor: comment node sanitization (1 new test) - JS tabs: missing panel, generated id, parent null, click no-panel (5 new tests) Code duplication fixes: - MeilisearchConsistencyCommand: extract diffAndReport() method (was duplicated 3x) - Email templates: extract _order_items_table.html.twig partial (shared by notification + cancelled) - SonarQube: exclude src/Entity/** from CPD (getters/setters duplication) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1954,6 +1954,94 @@ class AccountControllerTest extends WebTestCase
|
||||
return $category;
|
||||
}
|
||||
|
||||
public function testOrganizerFinanceStatsWithAllStatuses(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
$user->setStripeAccountId('acct_finance_'.uniqid());
|
||||
$user->setStripeChargesEnabled(true);
|
||||
$user->setCommissionRate(3.0);
|
||||
$em->flush();
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$event->setIsOnline(true);
|
||||
$category = $this->createCategory($em, $event);
|
||||
$billet = $this->createBillet($em, $category);
|
||||
$em->flush();
|
||||
|
||||
$statuses = [
|
||||
\App\Entity\BilletBuyer::STATUS_PAID,
|
||||
\App\Entity\BilletBuyer::STATUS_PENDING,
|
||||
\App\Entity\BilletBuyer::STATUS_REFUNDED,
|
||||
\App\Entity\BilletBuyer::STATUS_CANCELLED,
|
||||
];
|
||||
|
||||
foreach ($statuses as $i => $status) {
|
||||
$order = new \App\Entity\BilletBuyer();
|
||||
$order->setEvent($event);
|
||||
$order->setFirstName('F'.$i);
|
||||
$order->setLastName('L'.$i);
|
||||
$order->setEmail('f'.$i.'@test.fr');
|
||||
$order->setOrderNumber('2026-'.random_int(10000, 99999));
|
||||
$order->setTotalHT(1000);
|
||||
$order->setStatus($status);
|
||||
if (\App\Entity\BilletBuyer::STATUS_PAID === $status) {
|
||||
$order->setPaidAt(new \DateTimeImmutable());
|
||||
}
|
||||
|
||||
$item = new \App\Entity\BilletBuyerItem();
|
||||
$item->setBillet($billet);
|
||||
$item->setBilletName('Entree');
|
||||
$item->setQuantity(1);
|
||||
$item->setUnitPriceHT(1000);
|
||||
$order->addItem($item);
|
||||
$em->persist($order);
|
||||
}
|
||||
$em->flush();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte?tab=payouts');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testEditEventStatsTabWithSoldCounts(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$event->setIsOnline(true);
|
||||
$category = $this->createCategory($em, $event);
|
||||
$billet = $this->createBillet($em, $category);
|
||||
|
||||
$order = new \App\Entity\BilletBuyer();
|
||||
$order->setEvent($event);
|
||||
$order->setFirstName('Jean');
|
||||
$order->setLastName('Sold');
|
||||
$order->setEmail('sold@test.fr');
|
||||
$order->setOrderNumber('2026-'.random_int(10000, 99999));
|
||||
$order->setTotalHT(1000);
|
||||
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
||||
$order->setPaidAt(new \DateTimeImmutable());
|
||||
|
||||
$item = new \App\Entity\BilletBuyerItem();
|
||||
$item->setBillet($billet);
|
||||
$item->setBilletName('Entree');
|
||||
$item->setQuantity(3);
|
||||
$item->setUnitPriceHT(1000);
|
||||
$order->addItem($item);
|
||||
$em->persist($order);
|
||||
$em->flush();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
|
||||
@@ -530,4 +530,103 @@ class OrderControllerTest extends WebTestCase
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertArrayHasKey('redirect', $data);
|
||||
}
|
||||
|
||||
public function testCreateOrderEventEnded(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createOrga($em);
|
||||
|
||||
$event = new Event();
|
||||
$event->setAccount($user);
|
||||
$event->setTitle('Past Event');
|
||||
$event->setStartAt(new \DateTimeImmutable('-2 days'));
|
||||
$event->setEndAt(new \DateTimeImmutable('-1 day'));
|
||||
$event->setAddress('1 rue');
|
||||
$event->setZipcode('75001');
|
||||
$event->setCity('Paris');
|
||||
$event->setIsOnline(true);
|
||||
$em->persist($event);
|
||||
$em->flush();
|
||||
|
||||
$client->request('POST', '/evenement/'.$event->getId().'/commander', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([
|
||||
['billetId' => 1, 'qty' => 1],
|
||||
]));
|
||||
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertStringContainsString('termine', $data['error']);
|
||||
}
|
||||
|
||||
public function testCreateOrderInvalidCartStructure(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createOrga($em);
|
||||
[$event] = $this->createEventWithBillet($em, $user);
|
||||
|
||||
$client->request('POST', '/evenement/'.$event->getId().'/commander', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([
|
||||
['invalid' => 'data'],
|
||||
]));
|
||||
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
}
|
||||
|
||||
public function testGuestSubmitInvalidEmail(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createOrga($em);
|
||||
[$event, $billet] = $this->createEventWithBillet($em, $user);
|
||||
$order = $this->createOrder($em, $event, $billet);
|
||||
|
||||
$client->request('POST', '/commande/'.$order->getId().'/informations', [
|
||||
'first_name' => 'Jean',
|
||||
'last_name' => 'Dupont',
|
||||
'email' => 'not-an-email',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/commande/'.$order->getId().'/informations');
|
||||
}
|
||||
|
||||
public function testCreateOrderStockZeroSkipsBillet(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createOrga($em);
|
||||
|
||||
$event = new Event();
|
||||
$event->setAccount($user);
|
||||
$event->setTitle('Stock Zero Event');
|
||||
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
||||
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
||||
$event->setAddress('1 rue');
|
||||
$event->setZipcode('75001');
|
||||
$event->setCity('Paris');
|
||||
$event->setIsOnline(true);
|
||||
$em->persist($event);
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('Cat');
|
||||
$category->setEvent($event);
|
||||
$category->setStartAt(new \DateTimeImmutable('-1 day'));
|
||||
$category->setEndAt(new \DateTimeImmutable('+30 days'));
|
||||
$em->persist($category);
|
||||
|
||||
$billet = new Billet();
|
||||
$billet->setName('Sold Out');
|
||||
$billet->setCategory($category);
|
||||
$billet->setPriceHT(1500);
|
||||
$billet->setQuantity(0);
|
||||
$em->persist($billet);
|
||||
$em->flush();
|
||||
|
||||
$client->request('POST', '/evenement/'.$event->getId().'/commander', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([
|
||||
['billetId' => $billet->getId(), 'qty' => 1],
|
||||
]));
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertArrayHasKey('redirect', $data);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user