2026-03-19 08:55:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Tests\Controller;
|
|
|
|
|
|
|
|
|
|
use App\Entity\User;
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
|
|
|
|
|
|
|
|
class AccountControllerTest extends WebTestCase
|
|
|
|
|
{
|
|
|
|
|
public function testAccountRedirectsWhenNotAuthenticated(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAccountReturnsSuccessWhenAuthenticated(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
public function testAccountTicketsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=tickets');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 18:18:11 +01:00
|
|
|
public function testAccountTicketsTabWithTickets(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setUser($user);
|
|
|
|
|
$order->setFirstName($user->getFirstName());
|
|
|
|
|
$order->setLastName($user->getLastName());
|
|
|
|
|
$order->setEmail($user->getEmail());
|
Complete TASK_CHECKUP: security, UX, tests, coverage, accessibility, config externalization
Billetterie:
- Partial refund support (STATUS_PARTIALLY_REFUNDED, refundedAmount field, migration)
- Race condition fix: PESSIMISTIC_WRITE lock on stock decrement in transaction
- Idempotency key on PaymentIntent::create, reuse existing PI if stripeSessionId set
- Disable checkout when event ended (server 400 + template hide)
- Webhook deduplication via cache (24h TTL on stripe event.id)
- Email validation (filter_var) in OrderController guest flow
- JSON cart validation (structure check before processing)
- Invitation expiration after 7 days (isExpired method + landing page message)
- Stripe Checkout fallback when JS fails to load (noscript + redirect)
Config externalization:
- Move Stripe fees (STRIPE_FEE_RATE, STRIPE_FEE_FIXED) and admin email (ADMIN_EMAIL) to .env/services.yaml
- Replace all hardcoded contact@e-cosplay.fr across 13 files
- MailerService: getAdminEmail()/getAdminFrom(), default $from=null resolves to admin
UX & Accessibility:
- ARIA tabs: role=tablist/tab/tabpanel, aria-selected, keyboard nav (arrows, Home, End)
- aria-label on cart +/- buttons and editor toolbar buttons
- tabindex=0 on editor toolbar buttons for keyboard access
- data-confirm handler in app.js (was only in admin.js)
- Cart error feedback on checkout failure
- Billet designer save feedback (loading/success/error states)
- Stock polling every 30s with rupture/low stock badges
- Back to event link on payment page
Security:
- HTML sanitizer: BLOCKED_TAGS list (script, style, iframe, svg, etc.) - content fully removed
- Stripe polling timeout (15s max) with fallback redirect
- Rate limiting on public order access (20/5min)
- .catch() on all fetch() calls (sortable, billet-designer)
Tests (92% PHP, 100% JS lines):
- PCOV added to dev Dockerfile
- Test DB setup: .env.test with DATABASE_URL, Redis auth, Meilisearch key
- Rate limiter disabled in test env
- Makefile: test_db_setup, test_db_reset, run_test_php, run_test_coverage_php/js
- New tests: InvitationFlowTest (21), AuditServiceTest (4), ExportServiceTest (9), InvoiceServiceTest (4)
- New tests: SuspendedUserSubscriberTest, RateLimiterSubscriberTest, MeilisearchServiceTest
- New tests: Stripe webhook payment_failed (6) + charge.refunded (6)
- New tests: BilletBuyer refund, User suspended, OrganizerInvitation expiration
- JS tests: stock polling (6), data-confirm (2), copy-url restore (1), editor ARIA (2), XSS (9), tabs keyboard (9)
- ESLint + PHP CS Fixer: 0 errors
- SonarQube exclusions aligned with vitest coverage config
Infra:
- Meilisearch consistency command (app:meilisearch:check-consistency --fix) + cron daily 3am
- MeilisearchService: getAllDocumentIds(), listIndexes()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:14:06 +01:00
|
|
|
$order->setOrderNumber('2026-03-21-'.random_int(10000, 99999));
|
2026-03-21 18:18:11 +01:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test Billet');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test Billet');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=tickets');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
public function testAccountPurchasesTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=purchases');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAccountInvoicesTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=invoices');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAccountSettingsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=settings');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAccountSettingsSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/parametres', [
|
|
|
|
|
'first_name' => 'Updated',
|
|
|
|
|
'last_name' => 'Name',
|
|
|
|
|
'email' => $user->getEmail(),
|
|
|
|
|
'phone' => '0699887766',
|
|
|
|
|
'address' => '1 rue Test',
|
|
|
|
|
'postal_code' => '75001',
|
|
|
|
|
'city' => 'Paris',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=settings');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerEventsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=events');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerSubaccountsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=subaccounts');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerPayoutsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=payouts');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerSettingsDisablesNameFields(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/parametres', [
|
|
|
|
|
'email' => $user->getEmail(),
|
|
|
|
|
'phone' => '0699887766',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=settings');
|
|
|
|
|
}
|
|
|
|
|
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
public function testOrganizerNotApprovedShowsBlockingMessage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], false);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'en cours de validation');
|
|
|
|
|
}
|
|
|
|
|
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
public function testOrganizerDefaultTabIsEvents(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testStripeConnectRedirectsForNonOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/stripe-connect');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerWithoutStripeShowsSetupMessage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
2026-03-24 11:06:39 +01:00
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
2026-03-24 11:06:39 +01:00
|
|
|
$user->setStripeAccountId(null);
|
|
|
|
|
$user->setStripeChargesEnabled(false);
|
|
|
|
|
$user->setStripePayoutsEnabled(false);
|
|
|
|
|
$em->flush();
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$crawler = $client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Configuration Stripe requise');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerWithStripePendingShowsMessage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
$user->setStripeAccountId('acct_pending');
|
2026-03-24 11:06:39 +01:00
|
|
|
$user->setStripeChargesEnabled(false);
|
|
|
|
|
$user->setStripePayoutsEnabled(false);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$crawler = $client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'en cours de verification');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 11:06:39 +01:00
|
|
|
public function testOrganizerWithoutStripeRedirectsFromOrganizerTabs(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$user->setStripeAccountId(null);
|
|
|
|
|
$user->setStripeChargesEnabled(false);
|
|
|
|
|
$user->setStripePayoutsEnabled(false);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=events');
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Configuration Stripe requise');
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=subaccounts');
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Configuration Stripe requise');
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=payouts');
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Configuration Stripe requise');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testOrganizerWithoutStripeBlocksEventCreation(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$user->setStripeAccountId(null);
|
|
|
|
|
$user->setStripeChargesEnabled(false);
|
|
|
|
|
$user->setStripePayoutsEnabled(false);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/creer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte');
|
|
|
|
|
}
|
|
|
|
|
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
public function testOrganizerWithStripeActiveShowsSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
$user->setStripeAccountId('acct_active');
|
|
|
|
|
$user->setStripeChargesEnabled(true);
|
|
|
|
|
$user->setStripePayoutsEnabled(true);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$crawler = $client->request('GET', '/mon-compte');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Stripe Connect actif');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testStripeConnectReturn(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/stripe/connect/return');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testStripeConnectRefresh(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/stripe/connect/refresh');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/stripe-connect');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testStripeCancelResetsAccount(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
$user->setStripeAccountId('acct_cancel');
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/stripe-cancel');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte');
|
|
|
|
|
|
|
|
|
|
$em->refresh($user);
|
|
|
|
|
self::assertNull($user->getStripeAccountId());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:15:45 +01:00
|
|
|
public function testCreateSubAccountDeniedForNonOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/creer', [
|
|
|
|
|
'first_name' => 'Sub',
|
|
|
|
|
'last_name' => 'Test',
|
|
|
|
|
'email' => 'sub-denied-'.uniqid().'@example.com',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditSubAccountDeniedForWrongOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$orga1 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$orga2 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-wrong-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Wrong');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($orga1);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($orga2);
|
|
|
|
|
$client->request('GET', '/mon-compte/sous-compte/'.$sub->getId());
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditSubAccountSubmitDeniedForWrongOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$orga1 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$orga2 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-wrong2-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Wrong2');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($orga1);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($orga2);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/'.$sub->getId().'/modifier', [
|
|
|
|
|
'first_name' => 'Hack',
|
|
|
|
|
'last_name' => 'Attempt',
|
|
|
|
|
'email' => $sub->getEmail(),
|
|
|
|
|
'permissions' => ['scanner'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteSubAccountDeniedForWrongOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$orga1 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$orga2 = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-wrong3-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Wrong3');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($orga1);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($orga2);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/'.$sub->getId().'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
public function testCreateSubAccount(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$mailer = $this->createMock(\App\Service\MailerService::class);
|
|
|
|
|
$mailer->expects(self::once())->method('sendEmail');
|
|
|
|
|
static::getContainer()->set(\App\Service\MailerService::class, $mailer);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/creer', [
|
|
|
|
|
'first_name' => 'Sub',
|
|
|
|
|
'last_name' => 'Account',
|
|
|
|
|
'email' => 'sub-'.uniqid().'@example.com',
|
|
|
|
|
'permissions' => ['scanner', 'events'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=subaccounts');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditSubAccountPage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-edit-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Edit');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($user);
|
|
|
|
|
$sub->setSubAccountPermissions(['scanner']);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/sous-compte/'.$sub->getId());
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditSubAccountSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-submit-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Submit');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($user);
|
|
|
|
|
$sub->setSubAccountPermissions(['scanner']);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/'.$sub->getId().'/modifier', [
|
|
|
|
|
'first_name' => 'Updated',
|
|
|
|
|
'last_name' => 'Name',
|
|
|
|
|
'email' => $sub->getEmail(),
|
|
|
|
|
'permissions' => ['scanner', 'events', 'tickets'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=subaccounts');
|
|
|
|
|
|
|
|
|
|
$em->refresh($sub);
|
|
|
|
|
self::assertSame('Updated', $sub->getFirstName());
|
|
|
|
|
self::assertSame(['scanner', 'events', 'tickets'], $sub->getSubAccountPermissions());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteSubAccount(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$sub = new User();
|
|
|
|
|
$sub->setEmail('sub-del-'.uniqid().'@example.com');
|
|
|
|
|
$sub->setFirstName('Sub');
|
|
|
|
|
$sub->setLastName('Delete');
|
|
|
|
|
$sub->setPassword('$2y$13$hashed');
|
|
|
|
|
$sub->setParentOrganizer($user);
|
|
|
|
|
$em->persist($sub);
|
|
|
|
|
$em->flush();
|
|
|
|
|
$subId = $sub->getId();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/sous-compte/'.$subId.'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=subaccounts');
|
|
|
|
|
|
|
|
|
|
self::assertNull($em->getRepository(User::class)->find($subId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 11:00:08 +01:00
|
|
|
public function testOrganizerSettingsWithLogoUpload(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
|
|
|
|
|
$logoFile = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
|
|
|
__DIR__.'/../../public/logo.png',
|
|
|
|
|
'logo.png',
|
|
|
|
|
'image/png',
|
|
|
|
|
null,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$client->request('POST', '/mon-compte/parametres', [
|
|
|
|
|
'email' => $user->getEmail(),
|
|
|
|
|
'phone' => '0699887766',
|
|
|
|
|
], ['logo' => $logoFile]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=settings');
|
|
|
|
|
|
|
|
|
|
$em->refresh($user);
|
|
|
|
|
self::assertNotNull($user->getLogoName());
|
|
|
|
|
}
|
|
|
|
|
|
Add Event entity, create event page, and custom WYSIWYG editor component
- Create Event entity with fields: account, title, description (text), startAt, endAt, address, zipcode, city, eventMainPicture (VichUploader), isOnline, createdAt, updatedAt
- Create EventRepository
- Add migration for event table with all columns
- Add "Creer un evenement" button on account events tab
- Add create event page (/mon-compte/evenement/creer) with full form
- Build custom web component <e-ticket-editor> WYSIWYG editor:
- Toolbar: bold, italic, underline, paragraph, bullet list, remove formatting
- contentEditable div with HTML sync to hidden textarea
- HTML sanitizer (strips disallowed tags, XSS safe)
- Neo-brutalist CSS styling
- CSP compliant (no inline styles)
- Register editor in app.js via customElements.define
- Add editor CSS in app.scss
- Add 16 Event entity tests (all fields + isOnline + picture upload + updatedAt)
- Add 16 editor JS tests (sanitizer + custom element lifecycle)
- Add 3 AccountController tests (create event page, submit, access control)
- Update placeholders to generic examples (no association-specific data)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:49:24 +01:00
|
|
|
public function testCreateEventPageRequiresOrganizer(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/creer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateEventPageReturnsSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/creer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateEventSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/creer', [
|
|
|
|
|
'title' => 'Convention Test',
|
|
|
|
|
'description' => 'Un super evenement',
|
|
|
|
|
'start_at' => '2026-07-01T10:00',
|
|
|
|
|
'end_at' => '2026-07-01T18:00',
|
|
|
|
|
'address' => '42 rue de Saint-Quentin',
|
|
|
|
|
'zipcode' => '02800',
|
|
|
|
|
'city' => 'Beautor',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=events');
|
|
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$event = $freshEm->getRepository(\App\Entity\Event::class)->findOneBy(['title' => 'Convention Test']);
|
Add Event entity, create event page, and custom WYSIWYG editor component
- Create Event entity with fields: account, title, description (text), startAt, endAt, address, zipcode, city, eventMainPicture (VichUploader), isOnline, createdAt, updatedAt
- Create EventRepository
- Add migration for event table with all columns
- Add "Creer un evenement" button on account events tab
- Add create event page (/mon-compte/evenement/creer) with full form
- Build custom web component <e-ticket-editor> WYSIWYG editor:
- Toolbar: bold, italic, underline, paragraph, bullet list, remove formatting
- contentEditable div with HTML sync to hidden textarea
- HTML sanitizer (strips disallowed tags, XSS safe)
- Neo-brutalist CSS styling
- CSP compliant (no inline styles)
- Register editor in app.js via customElements.define
- Add editor CSS in app.scss
- Add 16 Event entity tests (all fields + isOnline + picture upload + updatedAt)
- Add 16 editor JS tests (sanitizer + custom element lifecycle)
- Add 3 AccountController tests (create event page, submit, access control)
- Update placeholders to generic examples (no association-specific data)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:49:24 +01:00
|
|
|
self::assertNotNull($event);
|
|
|
|
|
self::assertSame('Un super evenement', $event->getDescription());
|
|
|
|
|
self::assertSame('Beautor', $event->getCity());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:13:31 +01:00
|
|
|
public function testEditEventPageReturnsSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Edit Test');
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
|
|
|
|
$event->setAddress('1 rue test');
|
|
|
|
|
$event->setZipcode('75001');
|
|
|
|
|
$event->setCity('Paris');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Before Edit');
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
|
|
|
|
$event->setAddress('1 rue test');
|
|
|
|
|
$event->setZipcode('75001');
|
|
|
|
|
$event->setCity('Paris');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/modifier', [
|
|
|
|
|
'title' => 'After Edit',
|
|
|
|
|
'description' => 'Nouvelle description',
|
|
|
|
|
'start_at' => '2026-09-01T10:00',
|
|
|
|
|
'end_at' => '2026-09-01T18:00',
|
|
|
|
|
'address' => '2 rue modif',
|
|
|
|
|
'zipcode' => '69001',
|
|
|
|
|
'city' => 'Lyon',
|
|
|
|
|
'is_online' => '1',
|
|
|
|
|
]);
|
|
|
|
|
|
Add Category entity, edit event tabs (info/categories/stats/settings), CRUD categories
- Create Category entity: name, position (sortable), event, startAt, endAt, isActive()
- Default endAt: event.startAt - 1 day
- Add 4 tabs on edit event page: Informations, Categories/Billets, Statistiques, Parametres
- Add routes: add category, delete category, reorder categories (JSON API)
- Categories sorted by position, drag handle for future Sortable.js
- Active/Inactive badge based on date range
- Add migration for category table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:51:25 +01:00
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier');
|
2026-03-20 19:13:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($owner);
|
|
|
|
|
$event->setTitle('Owner 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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteEvent(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('To Delete');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
$eventId = $event->getId();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$eventId.'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte?tab=events');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testToggleEventOnline(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$user->setStripeChargesEnabled(true);
|
|
|
|
|
$user->setStripePayoutsEnabled(true);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Toggle Online');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/en-ligne');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testToggleEventOnlineBlockedWithoutStripe(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('No Stripe');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/en-ligne');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testToggleEventSecret(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Toggle Secret');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/secret');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 21:47:38 +01:00
|
|
|
public function testCreateEventWithPicture(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
|
|
|
|
|
$picture = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
|
|
|
__DIR__.'/../../public/logo.png',
|
|
|
|
|
'affiche.png',
|
|
|
|
|
'image/png',
|
|
|
|
|
null,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/creer', [
|
|
|
|
|
'title' => 'Event With Picture',
|
|
|
|
|
'description' => 'Description',
|
|
|
|
|
'start_at' => '2026-09-01T10:00',
|
|
|
|
|
'end_at' => '2026-09-01T18:00',
|
|
|
|
|
'address' => '1 rue test',
|
|
|
|
|
'zipcode' => '75001',
|
|
|
|
|
'city' => 'Paris',
|
|
|
|
|
], ['event_main_picture' => $picture]);
|
|
|
|
|
|
Add Category entity, edit event tabs (info/categories/stats/settings), CRUD categories
- Create Category entity: name, position (sortable), event, startAt, endAt, isActive()
- Default endAt: event.startAt - 1 day
- Add 4 tabs on edit event page: Informations, Categories/Billets, Statistiques, Parametres
- Add routes: add category, delete category, reorder categories (JSON API)
- Categories sorted by position, drag handle for future Sortable.js
- Active/Inactive badge based on date range
- Add migration for category table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:51:25 +01:00
|
|
|
self::assertResponseRedirects();
|
2026-03-20 21:47:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventWithPicture(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Edit With Pic');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
|
|
|
|
|
$picture = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
|
|
|
__DIR__.'/../../public/logo.png',
|
|
|
|
|
'affiche.png',
|
|
|
|
|
'image/png',
|
|
|
|
|
null,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/modifier', [
|
|
|
|
|
'title' => 'Edited With Pic',
|
|
|
|
|
'description' => 'New desc',
|
|
|
|
|
'start_at' => '2026-09-01T10:00',
|
|
|
|
|
'end_at' => '2026-09-01T18:00',
|
|
|
|
|
'address' => '2 rue',
|
|
|
|
|
'zipcode' => '69001',
|
|
|
|
|
'city' => 'Lyon',
|
|
|
|
|
], ['event_main_picture' => $picture]);
|
|
|
|
|
|
Add Category entity, edit event tabs (info/categories/stats/settings), CRUD categories
- Create Category entity: name, position (sortable), event, startAt, endAt, isActive()
- Default endAt: event.startAt - 1 day
- Add 4 tabs on edit event page: Informations, Categories/Billets, Statistiques, Parametres
- Add routes: add category, delete category, reorder categories (JSON API)
- Categories sorted by position, drag handle for future Sortable.js
- Active/Inactive badge based on date range
- Add migration for category table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:51:25 +01:00
|
|
|
self::assertResponseRedirects();
|
2026-03-20 21:47:38 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 21:25:53 +01:00
|
|
|
public function testEventsSearchReturnsSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte?tab=events&q=brocante');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testToggleOnlineDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($owner);
|
|
|
|
|
$event->setTitle('Toggle Denied');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/en-ligne');
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testToggleSecretDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($owner);
|
|
|
|
|
$event->setTitle('Secret Denied');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/secret');
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteEventDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($owner);
|
|
|
|
|
$event->setTitle('Delete Denied');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/supprimer');
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:35:42 +01:00
|
|
|
public function testAddCategory(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
|
|
|
|
'name' => 'VIP',
|
|
|
|
|
'start_at' => '2026-06-01T10:00',
|
|
|
|
|
'end_at' => '2026-07-31T18:00',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddCategoryEmptyName(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
|
|
|
|
'name' => '',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddCategoryDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
|
|
|
|
'name' => 'Hack',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddCategoryWithInvertedDates(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
|
|
|
|
'name' => 'Inverted',
|
|
|
|
|
'start_at' => '2026-08-01T10:00',
|
|
|
|
|
'end_at' => '2026-06-01T10:00',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
$category = $em->getRepository(\App\Entity\Category::class)->findOneBy(['name' => 'Inverted']);
|
|
|
|
|
self::assertNotNull($category);
|
|
|
|
|
self::assertGreaterThanOrEqual($category->getStartAt(), $category->getEndAt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditCategoryPage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditCategorySubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier', [
|
|
|
|
|
'name' => 'Updated Name',
|
|
|
|
|
'start_at' => '2026-06-01T10:00',
|
|
|
|
|
'end_at' => '2026-07-31T18:00',
|
|
|
|
|
'is_hidden' => '1',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
$em->refresh($category);
|
|
|
|
|
self::assertSame('Updated Name', $category->getName());
|
|
|
|
|
self::assertTrue($category->isHidden());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditCategoryWithInvertedDates(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier', [
|
|
|
|
|
'name' => 'Inverted Edit',
|
|
|
|
|
'start_at' => '2026-08-01T10:00',
|
|
|
|
|
'end_at' => '2026-06-01T10:00',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
$em->refresh($category);
|
|
|
|
|
self::assertGreaterThanOrEqual($category->getStartAt(), $category->getEndAt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditCategoryDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditCategoryNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/999999/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteCategory(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$categoryId = $category->getId();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$categoryId.'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
self::assertNull($em->getRepository(\App\Entity\Category::class)->find($categoryId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteCategoryDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testReorderCategories(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$cat1 = $this->createCategory($em, $event, 'Cat A', 0);
|
|
|
|
|
$cat2 = $this->createCategory($em, $event, 'Cat B', 1);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request(
|
|
|
|
|
'POST',
|
|
|
|
|
'/mon-compte/evenement/'.$event->getId().'/categorie/reorder',
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
['CONTENT_TYPE' => 'application/json'],
|
|
|
|
|
json_encode([$cat2->getId(), $cat1->getId()])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
|
|
|
|
|
$em->refresh($cat1);
|
|
|
|
|
$em->refresh($cat2);
|
|
|
|
|
self::assertSame(1, $cat1->getPosition());
|
|
|
|
|
self::assertSame(0, $cat2->getPosition());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testReorderCategoriesDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request(
|
|
|
|
|
'POST',
|
|
|
|
|
'/mon-compte/evenement/'.$event->getId().'/categorie/reorder',
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
['CONTENT_TYPE' => 'application/json'],
|
|
|
|
|
'[]'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventCategoriesTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
2026-03-21 13:30:38 +01:00
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$this->createBillet($em, $category);
|
2026-03-20 23:35:42 +01:00
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 18:19:11 +01:00
|
|
|
public function testEditEventStatsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventStatsTabWithOrders(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-03-21 18:26:15 +01:00
|
|
|
$order->setOrderNumber('2026-03-21-'.random_int(1000, 9999));
|
2026-03-21 18:19:11 +01:00
|
|
|
$order->setTotalHT(1500);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$order->setPaidAt(new \DateTimeImmutable());
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(2);
|
|
|
|
|
$item->setUnitPriceHT(750);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventStatsTabWithSearch(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats&q=dupont');
|
2026-04-01 20:19:52 +02:00
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventStatsTabWithTicketSearch(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=tickets&tq=jean');
|
2026-03-21 18:19:11 +01:00
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
Add Billet entity, BilletDesign, ticket designer, CRUD billets, commissions
- Create Billet entity: name, position, priceHT, quantity (nullable=unlimited),
isGeneratedBillet, hasDefinedExit, notBuyable, type (billet/reservation_brocante/vote),
stripeProductId, description, picture (VichUploader), category (ManyToOne CASCADE)
- Create BilletDesign entity (OneToOne Event): accentColor, invitationTitle, invitationColor
- Billet CRUD: add/edit/delete with access control, Stripe product sync on connected account
- Billet reorder: drag & drop with position field, refactored sortable.js for both categories and billets
- Ticket designer tab (custom offer only): accent color, invitation title/color, live iframe preview
- A4 ticket preview: 4 zones (HG infos+billet, HD affiche, BG association, BD sortie+invitation), fake QR code SVG
- Commission calculator JS: live breakdown of E-Ticket fee, Stripe fee (1.5%+0.25EUR), net amount
- Sales recap on categories tab: qty sold, total HT, total commissions, total net
- DisableProfilerSubscriber: disable web profiler toolbar on preview iframe
- CSP: allow self in frame-src and frame-ancestors for preview iframe
- Flysystem: dedicated billets.storage for billet images
- Upload accept restricted to png/jpeg/webp/gif (no HEIC)
- Makefile: add force_sql_dev command
- CLAUDE.md: add rule to never modify existing migrations
- Consolidate all migrations into single Version20260321111125
- Tests: BilletTest (20), BilletDesignTest (6), DisableProfilerSubscriberTest (5),
billet-designer.test.js (7), commission-calculator.test.js (7),
AccountControllerTest billet CRUD tests (11)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:19:46 +01:00
|
|
|
public function testAddBilletPage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/billet/ajouter');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddBilletSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/billet/ajouter', [
|
|
|
|
|
'name' => 'Entree VIP',
|
2026-03-21 12:27:00 +01:00
|
|
|
'price_ht' => '15.00',
|
Add Billet entity, BilletDesign, ticket designer, CRUD billets, commissions
- Create Billet entity: name, position, priceHT, quantity (nullable=unlimited),
isGeneratedBillet, hasDefinedExit, notBuyable, type (billet/reservation_brocante/vote),
stripeProductId, description, picture (VichUploader), category (ManyToOne CASCADE)
- Create BilletDesign entity (OneToOne Event): accentColor, invitationTitle, invitationColor
- Billet CRUD: add/edit/delete with access control, Stripe product sync on connected account
- Billet reorder: drag & drop with position field, refactored sortable.js for both categories and billets
- Ticket designer tab (custom offer only): accent color, invitation title/color, live iframe preview
- A4 ticket preview: 4 zones (HG infos+billet, HD affiche, BG association, BD sortie+invitation), fake QR code SVG
- Commission calculator JS: live breakdown of E-Ticket fee, Stripe fee (1.5%+0.25EUR), net amount
- Sales recap on categories tab: qty sold, total HT, total commissions, total net
- DisableProfilerSubscriber: disable web profiler toolbar on preview iframe
- CSP: allow self in frame-src and frame-ancestors for preview iframe
- Flysystem: dedicated billets.storage for billet images
- Upload accept restricted to png/jpeg/webp/gif (no HEIC)
- Makefile: add force_sql_dev command
- CLAUDE.md: add rule to never modify existing migrations
- Consolidate all migrations into single Version20260321111125
- Tests: BilletTest (20), BilletDesignTest (6), DisableProfilerSubscriberTest (5),
billet-designer.test.js (7), commission-calculator.test.js (7),
AccountControllerTest billet CRUD tests (11)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:19:46 +01:00
|
|
|
'is_generated_billet' => '1',
|
|
|
|
|
'description' => 'Acces backstage',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
$billet = $em->getRepository(\App\Entity\Billet::class)->findOneBy(['name' => 'Entree VIP']);
|
|
|
|
|
self::assertNotNull($billet);
|
|
|
|
|
self::assertSame(1500, $billet->getPriceHT());
|
|
|
|
|
self::assertTrue($billet->isGeneratedBillet());
|
|
|
|
|
self::assertSame('Acces backstage', $billet->getDescription());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddBilletDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/billet/ajouter');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddBilletCategoryNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/999999/billet/ajouter');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditBilletPage(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billet->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditBilletSubmit(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billet->getId().'/modifier', [
|
|
|
|
|
'name' => 'Entree Premium',
|
2026-03-21 12:27:00 +01:00
|
|
|
'price_ht' => '25.00',
|
Add Billet entity, BilletDesign, ticket designer, CRUD billets, commissions
- Create Billet entity: name, position, priceHT, quantity (nullable=unlimited),
isGeneratedBillet, hasDefinedExit, notBuyable, type (billet/reservation_brocante/vote),
stripeProductId, description, picture (VichUploader), category (ManyToOne CASCADE)
- Create BilletDesign entity (OneToOne Event): accentColor, invitationTitle, invitationColor
- Billet CRUD: add/edit/delete with access control, Stripe product sync on connected account
- Billet reorder: drag & drop with position field, refactored sortable.js for both categories and billets
- Ticket designer tab (custom offer only): accent color, invitation title/color, live iframe preview
- A4 ticket preview: 4 zones (HG infos+billet, HD affiche, BG association, BD sortie+invitation), fake QR code SVG
- Commission calculator JS: live breakdown of E-Ticket fee, Stripe fee (1.5%+0.25EUR), net amount
- Sales recap on categories tab: qty sold, total HT, total commissions, total net
- DisableProfilerSubscriber: disable web profiler toolbar on preview iframe
- CSP: allow self in frame-src and frame-ancestors for preview iframe
- Flysystem: dedicated billets.storage for billet images
- Upload accept restricted to png/jpeg/webp/gif (no HEIC)
- Makefile: add force_sql_dev command
- CLAUDE.md: add rule to never modify existing migrations
- Consolidate all migrations into single Version20260321111125
- Tests: BilletTest (20), BilletDesignTest (6), DisableProfilerSubscriberTest (5),
billet-designer.test.js (7), commission-calculator.test.js (7),
AccountControllerTest billet CRUD tests (11)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:19:46 +01:00
|
|
|
'is_generated_billet' => '1',
|
|
|
|
|
'description' => 'Acces VIP',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
$em->refresh($billet);
|
|
|
|
|
self::assertSame('Entree Premium', $billet->getName());
|
|
|
|
|
self::assertSame(2500, $billet->getPriceHT());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditBilletDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billet->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditBilletNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/billet/999999/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteBillet(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
$billetId = $billet->getId();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billetId.'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
|
|
|
|
|
|
|
|
|
self::assertNull($em->getRepository(\App\Entity\Billet::class)->find($billetId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeleteBilletDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billet->getId().'/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 13:16:44 +01:00
|
|
|
public function testDeleteBilletNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet/999999/supprimer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBilletPreview(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/billet-preview');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBilletPreviewDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/billet-preview');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSaveBilletDesign(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet-design', [
|
|
|
|
|
'accent_color' => '#ff0000',
|
|
|
|
|
'invitation_title' => 'VIP',
|
|
|
|
|
'invitation_color' => '#00ff00',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
|
|
|
|
|
$design = $em->getRepository(\App\Entity\BilletDesign::class)->findOneBy(['event' => $event]);
|
|
|
|
|
self::assertNotNull($design);
|
|
|
|
|
self::assertSame('#ff0000', $design->getAccentColor());
|
|
|
|
|
self::assertSame('VIP', $design->getInvitationTitle());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSaveBilletDesignUpdatesExisting(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$design = new \App\Entity\BilletDesign();
|
|
|
|
|
$design->setEvent($event);
|
|
|
|
|
$em->persist($design);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet-design', [
|
|
|
|
|
'accent_color' => '#aabbcc',
|
|
|
|
|
'invitation_title' => 'Pass',
|
|
|
|
|
'invitation_color' => '#112233',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
|
|
|
|
|
$em->refresh($design);
|
|
|
|
|
self::assertSame('#aabbcc', $design->getAccentColor());
|
|
|
|
|
self::assertSame('Pass', $design->getInvitationTitle());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSaveBilletDesignDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet-design', [
|
|
|
|
|
'accent_color' => '#ff0000',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testReorderBillets(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$b1 = $this->createBillet($em, $category, 'B1', 1000);
|
|
|
|
|
$b2 = $this->createBillet($em, $category, 'B2', 2000);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request(
|
|
|
|
|
'POST',
|
|
|
|
|
'/mon-compte/evenement/'.$event->getId().'/billet/reorder',
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
['CONTENT_TYPE' => 'application/json'],
|
|
|
|
|
json_encode([$b2->getId(), $b1->getId()])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
|
|
|
|
|
$em->refresh($b1);
|
|
|
|
|
$em->refresh($b2);
|
|
|
|
|
self::assertSame(1, $b1->getPosition());
|
|
|
|
|
self::assertSame(0, $b2->getPosition());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testReorderBilletsDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request(
|
|
|
|
|
'POST',
|
|
|
|
|
'/mon-compte/evenement/'.$event->getId().'/billet/reorder',
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
['CONTENT_TYPE' => 'application/json'],
|
|
|
|
|
'[]'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 13:30:38 +01:00
|
|
|
public function testAddBilletWithPicture(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$picture = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
|
|
|
__DIR__.'/../../public/logo.png',
|
|
|
|
|
'billet.png',
|
|
|
|
|
'image/png',
|
|
|
|
|
null,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/billet/ajouter', [
|
|
|
|
|
'name' => 'With Picture',
|
|
|
|
|
'price_ht' => '10.00',
|
|
|
|
|
'is_generated_billet' => '1',
|
|
|
|
|
], ['picture' => $picture]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditBilletWithPicture(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$picture = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
|
|
|
__DIR__.'/../../public/logo.png',
|
|
|
|
|
'billet.png',
|
|
|
|
|
'image/png',
|
|
|
|
|
null,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/billet/'.$billet->getId().'/modifier', [
|
|
|
|
|
'name' => 'Updated With Pic',
|
|
|
|
|
'price_ht' => '20.00',
|
|
|
|
|
'is_generated_billet' => '1',
|
|
|
|
|
], ['picture' => $picture]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 18:36:53 +01:00
|
|
|
public function testCreateInvitation(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
2026-03-21 18:47:03 +01:00
|
|
|
$billetOrderService->expects(self::once())->method('generateOrderTickets')->willReturnCallback(function (\App\Entity\BilletBuyer $order) use ($em, $billet) {
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Entree');
|
|
|
|
|
$ticket->setUnitPriceHT(0);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$order->setPaidAt(new \DateTimeImmutable());
|
|
|
|
|
$em->flush();
|
|
|
|
|
});
|
2026-03-21 18:36:53 +01:00
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation', [
|
|
|
|
|
'first_name' => 'Jean',
|
|
|
|
|
'last_name' => 'Invite',
|
|
|
|
|
'email' => 'invite@test.fr',
|
|
|
|
|
'items' => [
|
|
|
|
|
['billet_id' => $billet->getId(), 'quantity' => 2],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
2026-03-21 18:47:03 +01:00
|
|
|
|
|
|
|
|
$tickets = $em->getRepository(\App\Entity\BilletOrder::class)->findBy(['billet' => $billet]);
|
|
|
|
|
$lastTicket = end($tickets);
|
|
|
|
|
self::assertTrue($lastTicket->isInvitation());
|
2026-03-21 18:36:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateInvitationMultipleBillets(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet1 = $this->createBillet($em, $category, 'VIP', 2000);
|
|
|
|
|
$billet2 = $this->createBillet($em, $category, 'Standard', 1000);
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateOrderTickets');
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation', [
|
|
|
|
|
'first_name' => 'Marie',
|
|
|
|
|
'last_name' => 'Double',
|
|
|
|
|
'email' => 'marie@test.fr',
|
|
|
|
|
'items' => [
|
|
|
|
|
['billet_id' => $billet1->getId(), 'quantity' => 1],
|
|
|
|
|
['billet_id' => $billet2->getId(), 'quantity' => 3],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateInvitationEmptyFields(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation', [
|
|
|
|
|
'first_name' => '',
|
|
|
|
|
'last_name' => '',
|
|
|
|
|
'email' => '',
|
|
|
|
|
'items' => [],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateInvitationInvalidBillet(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation', [
|
|
|
|
|
'first_name' => 'Jean',
|
|
|
|
|
'last_name' => 'Test',
|
|
|
|
|
'email' => 'jean@test.fr',
|
|
|
|
|
'items' => [
|
|
|
|
|
['billet_id' => 999999, 'quantity' => 1],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateInvitationDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation', [
|
|
|
|
|
'first_name' => 'Hack',
|
|
|
|
|
'last_name' => 'Test',
|
|
|
|
|
'email' => 'hack@test.fr',
|
|
|
|
|
'items' => [],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditEventInvitationsTab(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 19:23:55 +01:00
|
|
|
public function testResendInvitation(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Invite');
|
|
|
|
|
$order->setLastName('Test');
|
|
|
|
|
$order->setEmail('invite@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
2026-03-21 19:23:55 +01:00
|
|
|
$order->setTotalHT(0);
|
|
|
|
|
$order->setIsInvitation(true);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation/'.$order->getId().'/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testResendInvitationDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Test');
|
|
|
|
|
$order->setLastName('Test');
|
|
|
|
|
$order->setEmail('test@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
2026-03-21 19:23:55 +01:00
|
|
|
$order->setTotalHT(0);
|
|
|
|
|
$order->setIsInvitation(true);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation/'.$order->getId().'/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testResendInvitationNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/invitation/999999/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 18:35:35 +01:00
|
|
|
public function testCancelOrder(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
2026-03-21 18:35:35 +01:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
Complete TASK_CHECKUP: security, UX, tests, coverage, accessibility, config externalization
Billetterie:
- Partial refund support (STATUS_PARTIALLY_REFUNDED, refundedAmount field, migration)
- Race condition fix: PESSIMISTIC_WRITE lock on stock decrement in transaction
- Idempotency key on PaymentIntent::create, reuse existing PI if stripeSessionId set
- Disable checkout when event ended (server 400 + template hide)
- Webhook deduplication via cache (24h TTL on stripe event.id)
- Email validation (filter_var) in OrderController guest flow
- JSON cart validation (structure check before processing)
- Invitation expiration after 7 days (isExpired method + landing page message)
- Stripe Checkout fallback when JS fails to load (noscript + redirect)
Config externalization:
- Move Stripe fees (STRIPE_FEE_RATE, STRIPE_FEE_FIXED) and admin email (ADMIN_EMAIL) to .env/services.yaml
- Replace all hardcoded contact@e-cosplay.fr across 13 files
- MailerService: getAdminEmail()/getAdminFrom(), default $from=null resolves to admin
UX & Accessibility:
- ARIA tabs: role=tablist/tab/tabpanel, aria-selected, keyboard nav (arrows, Home, End)
- aria-label on cart +/- buttons and editor toolbar buttons
- tabindex=0 on editor toolbar buttons for keyboard access
- data-confirm handler in app.js (was only in admin.js)
- Cart error feedback on checkout failure
- Billet designer save feedback (loading/success/error states)
- Stock polling every 30s with rupture/low stock badges
- Back to event link on payment page
Security:
- HTML sanitizer: BLOCKED_TAGS list (script, style, iframe, svg, etc.) - content fully removed
- Stripe polling timeout (15s max) with fallback redirect
- Rate limiting on public order access (20/5min)
- .catch() on all fetch() calls (sortable, billet-designer)
Tests (92% PHP, 100% JS lines):
- PCOV added to dev Dockerfile
- Test DB setup: .env.test with DATABASE_URL, Redis auth, Meilisearch key
- Rate limiter disabled in test env
- Makefile: test_db_setup, test_db_reset, run_test_php, run_test_coverage_php/js
- New tests: InvitationFlowTest (21), AuditServiceTest (4), ExportServiceTest (9), InvoiceServiceTest (4)
- New tests: SuspendedUserSubscriberTest, RateLimiterSubscriberTest, MeilisearchServiceTest
- New tests: Stripe webhook payment_failed (6) + charge.refunded (6)
- New tests: BilletBuyer refund, User suspended, OrganizerInvitation expiration
- JS tests: stock polling (6), data-confirm (2), copy-url restore (1), editor ARIA (2), XSS (9), tabs keyboard (9)
- ESLint + PHP CS Fixer: 0 errors
- SonarQube exclusions aligned with vitest coverage config
Infra:
- Meilisearch consistency command (app:meilisearch:check-consistency --fix) + cron daily 3am
- MeilisearchService: getAllDocumentIds(), listIndexes()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:14:06 +01:00
|
|
|
$mailer = $this->createMock(\App\Service\MailerService::class);
|
|
|
|
|
static::getContainer()->set(\App\Service\MailerService::class, $mailer);
|
|
|
|
|
|
2026-03-21 18:35:35 +01:00
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/commande/'.$order->getId().'/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats');
|
|
|
|
|
|
|
|
|
|
$em->refresh($order);
|
|
|
|
|
self::assertSame(\App\Entity\BilletBuyer::STATUS_CANCELLED, $order->getStatus());
|
|
|
|
|
|
|
|
|
|
$em->refresh($ticket);
|
|
|
|
|
self::assertSame(\App\Entity\BilletOrder::STATE_INVALID, $ticket->getState());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCancelOrderDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Test');
|
|
|
|
|
$order->setLastName('Test');
|
|
|
|
|
$order->setEmail('test@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
2026-03-21 18:35:35 +01:00
|
|
|
$order->setTotalHT(0);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/commande/'.$order->getId().'/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCancelOrderNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/commande/999999/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
Add Billet entity, BilletDesign, ticket designer, CRUD billets, commissions
- Create Billet entity: name, position, priceHT, quantity (nullable=unlimited),
isGeneratedBillet, hasDefinedExit, notBuyable, type (billet/reservation_brocante/vote),
stripeProductId, description, picture (VichUploader), category (ManyToOne CASCADE)
- Create BilletDesign entity (OneToOne Event): accentColor, invitationTitle, invitationColor
- Billet CRUD: add/edit/delete with access control, Stripe product sync on connected account
- Billet reorder: drag & drop with position field, refactored sortable.js for both categories and billets
- Ticket designer tab (custom offer only): accent color, invitation title/color, live iframe preview
- A4 ticket preview: 4 zones (HG infos+billet, HD affiche, BG association, BD sortie+invitation), fake QR code SVG
- Commission calculator JS: live breakdown of E-Ticket fee, Stripe fee (1.5%+0.25EUR), net amount
- Sales recap on categories tab: qty sold, total HT, total commissions, total net
- DisableProfilerSubscriber: disable web profiler toolbar on preview iframe
- CSP: allow self in frame-src and frame-ancestors for preview iframe
- Flysystem: dedicated billets.storage for billet images
- Upload accept restricted to png/jpeg/webp/gif (no HEIC)
- Makefile: add force_sql_dev command
- CLAUDE.md: add rule to never modify existing migrations
- Consolidate all migrations into single Version20260321111125
- Tests: BilletTest (20), BilletDesignTest (6), DisableProfilerSubscriberTest (5),
billet-designer.test.js (7), commission-calculator.test.js (7),
AccountControllerTest billet CRUD tests (11)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:19:46 +01:00
|
|
|
private function createBillet(EntityManagerInterface $em, \App\Entity\Category $category, string $name = 'Test Billet', int $priceHT = 1000): \App\Entity\Billet
|
|
|
|
|
{
|
|
|
|
|
$billet = new \App\Entity\Billet();
|
|
|
|
|
$billet->setName($name);
|
|
|
|
|
$billet->setCategory($category);
|
|
|
|
|
$billet->setPriceHT($priceHT);
|
|
|
|
|
$em->persist($billet);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
return $billet;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:35:42 +01:00
|
|
|
private function createEvent(EntityManagerInterface $em, User $user): \App\Entity\Event
|
|
|
|
|
{
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Test Event '.uniqid());
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
|
|
|
|
$event->setAddress('1 rue test');
|
|
|
|
|
$event->setZipcode('75001');
|
|
|
|
|
$event->setCity('Paris');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
return $event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function createCategory(EntityManagerInterface $em, \App\Entity\Event $event, string $name = 'Test Cat', int $position = 0): \App\Entity\Category
|
|
|
|
|
{
|
|
|
|
|
$category = new \App\Entity\Category();
|
|
|
|
|
$category->setName($name);
|
|
|
|
|
$category->setEvent($event);
|
|
|
|
|
$category->setPosition($position);
|
|
|
|
|
$category->setStartAt(new \DateTimeImmutable('2026-06-01 10:00'));
|
|
|
|
|
$category->setEndAt(new \DateTimeImmutable('2026-07-31 18:00'));
|
|
|
|
|
$em->persist($category);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
return $category;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 18:12:18 +01:00
|
|
|
public function testEventQrCode(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/qrcode');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSame('image/png', $client->getResponse()->headers->get('Content-Type'));
|
|
|
|
|
self::assertStringContainsString('attachment', $client->getResponse()->headers->get('Content-Disposition'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEventQrCodeDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$otherUser = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($otherUser);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/qrcode');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)
Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels
Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
|
|
|
public function testGetAllowedBilletTypesBasic(): void
|
|
|
|
|
{
|
2026-04-03 09:35:26 +02:00
|
|
|
$types = \App\Controller\AccountEventCatalogController::getAllowedBilletTypes('basic');
|
Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)
Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels
Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
|
|
|
self::assertSame(['billet', 'reservation_brocante', 'vote'], $types);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetAllowedBilletTypesSurMesure(): void
|
|
|
|
|
{
|
2026-04-03 09:35:26 +02:00
|
|
|
$types = \App\Controller\AccountEventCatalogController::getAllowedBilletTypes('sur-mesure');
|
Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)
Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels
Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
|
|
|
self::assertSame(['billet', 'reservation_brocante', 'vote'], $types);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetAllowedBilletTypesFree(): void
|
|
|
|
|
{
|
2026-04-03 09:35:26 +02:00
|
|
|
$types = \App\Controller\AccountEventCatalogController::getAllowedBilletTypes('free');
|
Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)
Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels
Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
|
|
|
self::assertSame(['billet'], $types);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetAllowedBilletTypesNull(): void
|
|
|
|
|
{
|
2026-04-03 09:35:26 +02:00
|
|
|
$types = \App\Controller\AccountEventCatalogController::getAllowedBilletTypes(null);
|
Add test coverage for remaining controllers, fix label accessibility, refactor duplicated code
New tests (47 added, 622 total):
- MonitorMessengerCommand: no failures, failures with email, null error, multiple (4)
- UnsubscribeController: unsubscribe with invitations refused + admin notified (1)
- AdminController: suspend/reactivate orga, orders page with filters, logs, invite orga submit/empty, delete/resend invitation, export CSV/PDF (13)
- AccountController: export CSV/PDF, getAllowedBilletTypes (free/basic/sur-mesure/null), billet type restriction, finance stats all statuses, soldCounts (9)
- HomeController: city filter, date filter, all filters combined, stock route (4)
- OrderController: event ended, invalid cart JSON, invalid email, stock zero (4)
- MailerService: getAdminEmail, getAdminFrom (2)
- JS: comment node, tabs missing panel/id/parent, cart stock polling edge cases (10)
Accessibility fixes:
- events.html.twig: add for/id on search, city, date labels
- admin/orders.html.twig: add for/id on search, status labels
Code quality:
- cart.js: remove dead ternaire branch (max > 10 always plural)
- tabs.js: use optional chaining for tablist?.setAttribute
- MeilisearchConsistencyCommand: extract diffAndReport() (was duplicated 3x)
- Email templates: extract _order_items_table.html.twig partial
- SonarQube: exclude src/Entity/** from CPD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:11:07 +01:00
|
|
|
self::assertSame(['billet'], $types);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddBilletTypeRestriction(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$user->setOffer('free');
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/billet/ajouter', [
|
|
|
|
|
'name' => 'Vote Interdit',
|
|
|
|
|
'price_ht' => '5',
|
|
|
|
|
'type' => 'vote',
|
|
|
|
|
'is_generated_billet' => '1',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects();
|
|
|
|
|
|
|
|
|
|
$billet = $em->getRepository(\App\Entity\Billet::class)->findOneBy(['name' => 'Vote Interdit']);
|
|
|
|
|
self::assertNotNull($billet);
|
|
|
|
|
self::assertSame('billet', $billet->getType());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testExportCsv(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/export/2026/3');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertStringContainsString('text/csv', $client->getResponse()->headers->get('Content-Type'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testExportPdf(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/export/2026/3/pdf');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertStringContainsString('application/pdf', $client->getResponse()->headers->get('Content-Type'));
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-23 11:44:13 +01:00
|
|
|
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);
|
2026-03-23 14:47:02 +01:00
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Entree');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
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>
2026-03-23 11:44:13 +01:00
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=stats');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 09:04:27 +01:00
|
|
|
public function testRootCanEditEventOfAnotherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$root = $this->createUser(['ROLE_ROOT']);
|
|
|
|
|
|
|
|
|
|
$event = new \App\Entity\Event();
|
|
|
|
|
$event->setAccount($owner);
|
|
|
|
|
$event->setTitle('Owner Event Root');
|
|
|
|
|
$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');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($root);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
// ---- downloadTicket tests ----
|
|
|
|
|
|
|
|
|
|
public function testDownloadTicketSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generatePdf')->willReturn('%PDF-1.4 fake');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/telecharger');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertResponseHeaderSame('Content-Type', 'application/pdf');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDownloadTicketDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/telecharger');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDownloadTicketNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/ticket/999999/telecharger');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- resendTicket tests ----
|
|
|
|
|
|
|
|
|
|
public function testResendTicketSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=tickets');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testResendTicketDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testResendTicketNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/999999/renvoyer');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- cancelTicket tests ----
|
|
|
|
|
|
|
|
|
|
public function testCancelTicketSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=tickets');
|
|
|
|
|
|
|
|
|
|
$em->refresh($ticket);
|
|
|
|
|
self::assertSame(\App\Entity\BilletOrder::STATE_INVALID, $ticket->getState());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCancelTicketDeniedForOtherUser(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $owner);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean@test.fr');
|
2026-04-01 20:04:21 +02:00
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(1);
|
|
|
|
|
$item->setUnitPriceHT(1000);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(1000);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($other);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/'.$ticket->getId().'/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCancelTicketNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/ticket/999999/annuler');
|
|
|
|
|
|
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- createAccreditation tests ----
|
|
|
|
|
|
|
|
|
|
public function testCreateAccreditationStaffSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
2026-04-02 14:02:10 +02:00
|
|
|
$billetOrderService->expects(self::once())->method('generateOrderTickets')->willReturnCallback(function (\App\Entity\BilletBuyer $order) use ($em) {
|
|
|
|
|
$billet = $em->getRepository(\App\Entity\Billet::class)->findOneBy(['category' => $order->getItems()->first()->getBillet()->getCategory()]);
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName($billet->getName());
|
|
|
|
|
$ticket->setUnitPriceHT(0);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$order->setPaidAt(new \DateTimeImmutable());
|
|
|
|
|
$em->flush();
|
|
|
|
|
});
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/accreditation', [
|
|
|
|
|
'accreditation_type' => 'staff',
|
|
|
|
|
'first_name' => 'Alice',
|
|
|
|
|
'last_name' => 'Martin',
|
|
|
|
|
'email' => 'alice@test.fr',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
2026-04-02 14:02:10 +02:00
|
|
|
|
|
|
|
|
$tickets = $em->getRepository(\App\Entity\BilletOrder::class)->findBy(['billetBuyer' => $em->getRepository(\App\Entity\BilletBuyer::class)->findOneBy(['email' => 'alice@test.fr'])]);
|
|
|
|
|
self::assertNotEmpty($tickets);
|
|
|
|
|
self::assertTrue($tickets[0]->isInvitation());
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateAccreditationExposantSuccess(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateOrderTickets');
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/accreditation', [
|
|
|
|
|
'accreditation_type' => 'exposant',
|
|
|
|
|
'first_name' => 'Bob',
|
|
|
|
|
'last_name' => 'Durand',
|
|
|
|
|
'email' => 'bob@test.fr',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateAccreditationEmptyFields(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/accreditation', [
|
|
|
|
|
'accreditation_type' => 'staff',
|
|
|
|
|
'first_name' => '',
|
|
|
|
|
'last_name' => '',
|
|
|
|
|
'email' => '',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateAccreditationNoCategories(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
// No category created
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/accreditation', [
|
|
|
|
|
'accreditation_type' => 'staff',
|
|
|
|
|
'first_name' => 'Alice',
|
|
|
|
|
'last_name' => 'Martin',
|
|
|
|
|
'email' => 'alice@test.fr',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCreateAccreditationInvalidTypeDefaultsToStaff(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
|
|
|
|
|
$billetOrderService = $this->createMock(\App\Service\BilletOrderService::class);
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateOrderTickets');
|
|
|
|
|
$billetOrderService->expects(self::once())->method('generateAndSendTickets');
|
|
|
|
|
static::getContainer()->set(\App\Service\BilletOrderService::class, $billetOrderService);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/accreditation', [
|
|
|
|
|
'accreditation_type' => 'invalid_type',
|
|
|
|
|
'first_name' => 'Alice',
|
|
|
|
|
'last_name' => 'Martin',
|
|
|
|
|
'email' => 'alice@test.fr',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=invitations');
|
|
|
|
|
|
|
|
|
|
// Verify the billet was created with 'staff' label (default)
|
|
|
|
|
$billets = $em->getRepository(\App\Entity\Billet::class)->findBy(['category' => $category, 'type' => 'staff']);
|
|
|
|
|
self::assertNotEmpty($billets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- eventAttestation tests ----
|
|
|
|
|
|
|
|
|
|
public function testEventAttestationWithCategories(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/attestation', [
|
|
|
|
|
'categories' => [$category->getId()],
|
|
|
|
|
'mode' => 'simple',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertResponseHeaderSame('Content-Type', 'application/pdf');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEventAttestationWithBillets(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/attestation', [
|
|
|
|
|
'billets' => [$billet->getId()],
|
|
|
|
|
'mode' => 'detail',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertResponseHeaderSame('Content-Type', 'application/pdf');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEventAttestationEmptySelection(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/attestation', [
|
|
|
|
|
'categories' => [],
|
|
|
|
|
'billets' => [],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=attestation');
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 10:02:33 +02:00
|
|
|
public function testEventAttestationWithSoldTickets(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
|
|
|
|
$user->setCompanyName('Test Asso');
|
|
|
|
|
$user->setSiret('12345678901234');
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$event = $this->createEvent($em, $user);
|
|
|
|
|
$category = $this->createCategory($em, $event);
|
|
|
|
|
$billet = $this->createBillet($em, $category);
|
|
|
|
|
|
|
|
|
|
$order = new \App\Entity\BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName('Jean');
|
|
|
|
|
$order->setLastName('Dupont');
|
|
|
|
|
$order->setEmail('jean-att@test.fr');
|
|
|
|
|
$order->setOrderNumber('T-'.substr(uniqid(), -7));
|
|
|
|
|
$order->setTotalHT(1000);
|
|
|
|
|
$order->setStatus(\App\Entity\BilletBuyer::STATUS_PAID);
|
|
|
|
|
$order->setPaidAt(new \DateTimeImmutable());
|
|
|
|
|
|
|
|
|
|
$item = new \App\Entity\BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName('Test');
|
|
|
|
|
$item->setQuantity(2);
|
|
|
|
|
$item->setUnitPriceHT(500);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$ticket = new \App\Entity\BilletOrder();
|
|
|
|
|
$ticket->setBilletBuyer($order);
|
|
|
|
|
$ticket->setBillet($billet);
|
|
|
|
|
$ticket->setBilletName('Test');
|
|
|
|
|
$ticket->setUnitPriceHT(500);
|
|
|
|
|
$em->persist($ticket);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->loginUser($user);
|
|
|
|
|
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/attestation', [
|
|
|
|
|
'categories' => [$category->getId()],
|
|
|
|
|
'mode' => 'detail',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertStringContainsString('application/pdf', $client->getResponse()->headers->get('Content-Type'));
|
|
|
|
|
}
|
|
|
|
|
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
/**
|
|
|
|
|
* @param list<string> $roles
|
|
|
|
|
*/
|
|
|
|
|
private function createUser(array $roles = [], bool $approved = false): User
|
2026-03-19 08:55:53 +01:00
|
|
|
{
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$user = new User();
|
|
|
|
|
$user->setEmail('test-account-'.uniqid().'@example.com');
|
|
|
|
|
$user->setFirstName('Test');
|
|
|
|
|
$user->setLastName('User');
|
|
|
|
|
$user->setPassword('$2y$13$hashed');
|
Refactor Stripe integration: single Connect webhook, account pages, cleanup
Stripe webhook:
- Single webhook endpoint /stripe/webhook for Connect + payment events
- v2 Connect events configured manually in Stripe Dashboard (not via API)
- account.updated syncs charges_enabled/payouts_enabled via API retrieve
- Remove StripeSyncCommand and saveWebhookSecret (secret managed via Ansible vault)
Account page (/mon-compte):
- Buyer tabs: Billets, Achats, Factures, Parametres
- Organizer tabs: Evenements/Brocantes, Sous-comptes, Virements + buyer tabs
- Stripe Connect status banner: setup required, pending verification, active, refused
- Stripe Connect onboarding: create account, complete verification (GET links)
- Dashboard Stripe: opens in new tab via createLoginLink (Express dashboard)
- Cancel/close Stripe account: deletes via API + resets local fields
- Stripe required message on events/subaccounts/payouts tabs when not active
- Settings: organizer fields locked (name, address), email/phone editable
- Return/refresh routes for Stripe Connect onboarding flow
- Error handling with flash messages on all Stripe operations
- Auto-sync Stripe status on /mon-compte visit
StripeService cleanup:
- Remove syncWebhook, saveWebhookSecret, getWebhookUrl, projectDir
- Add deleteAccount method
- Keep: verifyWebhookSignature, createAccountConnect, createAccountLink, createLoginLink
Security:
- Add connect.stripe.com and dashboard.stripe.com to nelmio whitelist
- Add STRIPE_SK, STRIPE_WEBHOOK_SECRET, OUTSIDE_URL to .env.test
Tests: 19 AccountControllerTest, 4 StripeWebhookControllerTest, 1 StripeServiceTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:41:31 +01:00
|
|
|
$user->setRoles($roles);
|
2026-03-19 08:55:53 +01:00
|
|
|
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
if ($approved) {
|
|
|
|
|
$user->setIsApproved(true);
|
2026-03-24 11:06:39 +01:00
|
|
|
if (\in_array('ROLE_ORGANIZER', $roles, true)) {
|
|
|
|
|
$user->setStripeAccountId('acct_test_'.uniqid());
|
|
|
|
|
$user->setStripeChargesEnabled(true);
|
|
|
|
|
$user->setStripePayoutsEnabled(true);
|
|
|
|
|
}
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 08:55:53 +01:00
|
|
|
$em->persist($user);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
return $user;
|
|
|
|
|
}
|
|
|
|
|
}
|