Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Controller;
|
|
|
|
|
|
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
|
|
|
use App\Entity\Billet;
|
2026-03-21 17:01:13 +01:00
|
|
|
use App\Entity\BilletBuyer;
|
2026-03-21 17:48:51 +01:00
|
|
|
use App\Entity\BilletBuyerItem;
|
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
|
|
|
use App\Entity\BilletDesign;
|
2026-03-21 17:01:13 +01:00
|
|
|
use App\Entity\BilletOrder;
|
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
|
|
|
use App\Entity\Category;
|
2026-03-20 19:15:22 +01:00
|
|
|
use App\Entity\Event;
|
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
|
|
|
use App\Entity\Payout;
|
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
|
|
|
use App\Entity\User;
|
2026-03-21 17:27:18 +01:00
|
|
|
use App\Service\BilletOrderService;
|
2026-03-20 17:18:45 +01:00
|
|
|
use App\Service\EventIndexService;
|
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
|
|
|
use App\Service\MailerService;
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
use App\Service\OrderIndexService;
|
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
|
|
|
use App\Service\PayoutPdfService;
|
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
|
|
|
use App\Service\StripeService;
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
2026-03-20 17:02:56 +01:00
|
|
|
use Knp\Component\Pager\PaginatorInterface;
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
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
|
|
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
|
|
|
|
|
|
#[IsGranted('ROLE_USER')]
|
|
|
|
|
class AccountController extends AbstractController
|
|
|
|
|
{
|
2026-03-20 11:00:08 +01:00
|
|
|
private const BREADCRUMB_HOME = ['name' => 'Accueil', 'url' => '/'];
|
|
|
|
|
private const BREADCRUMB_ACCOUNT = ['name' => 'Mon compte', 'url' => '/mon-compte'];
|
2026-03-21 13:10:40 +01:00
|
|
|
private const EVENT_BASE_URL = '/mon-compte/evenement/';
|
2026-03-21 13:11:54 +01:00
|
|
|
private const EVENT_CATEGORIES_SUFFIX = '/modifier?tab=categories';
|
2026-03-20 11:02:16 +01:00
|
|
|
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
#[Route('/mon-compte', name: 'app_account')]
|
2026-03-20 21:33:37 +01:00
|
|
|
public function index(Request $request, StripeService $stripeService, EntityManagerInterface $em, PaginatorInterface $paginator, EventIndexService $eventIndex): Response
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
{
|
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
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
$isOrganizer = $this->isGranted('ROLE_ORGANIZER');
|
|
|
|
|
$defaultTab = $isOrganizer ? 'events' : 'tickets';
|
|
|
|
|
$tab = $request->query->getString('tab', $defaultTab);
|
|
|
|
|
|
|
|
|
|
if ($isOrganizer && $user->getStripeAccountId() && (!$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled())) {
|
2026-03-20 00:05:17 +01:00
|
|
|
try { // @codeCoverageIgnoreStart
|
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
|
|
|
$account = $stripeService->getClient()->accounts->retrieve($user->getStripeAccountId());
|
|
|
|
|
$user->setStripeChargesEnabled((bool) $account->charges_enabled);
|
|
|
|
|
$user->setStripePayoutsEnabled((bool) $account->payouts_enabled);
|
|
|
|
|
$em->flush();
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
// Stripe API unavailable, keep current status
|
2026-03-20 00:05:17 +01:00
|
|
|
} // @codeCoverageIgnoreEnd
|
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
|
|
|
}
|
|
|
|
|
|
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
|
|
|
$payouts = [];
|
|
|
|
|
$subAccounts = [];
|
2026-03-20 17:02:56 +01:00
|
|
|
$events = [];
|
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 ($isOrganizer) {
|
|
|
|
|
$payouts = $em->getRepository(Payout::class)->findBy(
|
|
|
|
|
['organizer' => $user],
|
|
|
|
|
['createdAt' => 'DESC'],
|
|
|
|
|
);
|
|
|
|
|
$subAccounts = $em->getRepository(User::class)->findBy(
|
|
|
|
|
['parentOrganizer' => $user],
|
|
|
|
|
['createdAt' => 'DESC'],
|
|
|
|
|
);
|
2026-03-20 17:23:01 +01:00
|
|
|
$searchQuery = $request->query->getString('q', '');
|
2026-03-20 21:33:37 +01:00
|
|
|
$eventsQuery = $eventIndex->searchEvents('event_'.$user->getId(), $searchQuery, ['account' => $user]);
|
2026-03-20 17:02:56 +01:00
|
|
|
$events = $paginator->paginate($eventsQuery, $request->query->getInt('page', 1), 10);
|
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-21 17:01:13 +01:00
|
|
|
$orders = $em->getRepository(BilletBuyer::class)->findBy(
|
|
|
|
|
['user' => $user],
|
|
|
|
|
['createdAt' => 'DESC'],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$userTickets = [];
|
|
|
|
|
foreach ($orders as $order) {
|
|
|
|
|
$tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
|
|
|
|
|
foreach ($tickets as $ticket) {
|
|
|
|
|
$userTickets[] = $ticket;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
return $this->render('account/index.html.twig', [
|
|
|
|
|
'tab' => $tab,
|
|
|
|
|
'isOrganizer' => $isOrganizer,
|
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
|
|
|
'payouts' => $payouts,
|
|
|
|
|
'subAccounts' => $subAccounts,
|
2026-03-20 17:02:56 +01:00
|
|
|
'events' => $events,
|
2026-03-21 17:01:13 +01:00
|
|
|
'orders' => $orders,
|
|
|
|
|
'userTickets' => $userTickets,
|
Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates
- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:44:31 +01:00
|
|
|
'breadcrumbs' => [
|
2026-03-20 11:00:08 +01:00
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates
- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:44:31 +01:00
|
|
|
],
|
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
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/parametres', name: 'app_account_settings', methods: ['POST'])]
|
|
|
|
|
public function settings(Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
$isOrganizer = $this->isGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
if (!$isOrganizer) {
|
|
|
|
|
$user->setFirstName(trim($request->request->getString('first_name')));
|
|
|
|
|
$user->setLastName(trim($request->request->getString('last_name')));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$user->setEmail(trim($request->request->getString('email')));
|
|
|
|
|
$user->setPhone(trim($request->request->getString('phone')));
|
|
|
|
|
|
|
|
|
|
if (!$isOrganizer) {
|
|
|
|
|
$user->setAddress(trim($request->request->getString('address')));
|
|
|
|
|
$user->setPostalCode(trim($request->request->getString('postal_code')));
|
|
|
|
|
$user->setCity(trim($request->request->getString('city')));
|
|
|
|
|
}
|
|
|
|
|
|
Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates
- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:44:31 +01:00
|
|
|
if ($isOrganizer) {
|
|
|
|
|
$user->setWebsite(trim($request->request->getString('website')));
|
|
|
|
|
$user->setFacebook(trim($request->request->getString('facebook')));
|
|
|
|
|
$user->setInstagram(trim($request->request->getString('instagram')));
|
|
|
|
|
$user->setTwitter(trim($request->request->getString('twitter')));
|
|
|
|
|
$user->setTiktok(trim($request->request->getString('tiktok')));
|
|
|
|
|
|
|
|
|
|
$logoFile = $request->files->get('logo');
|
|
|
|
|
if ($logoFile) {
|
|
|
|
|
$user->setLogoFile($logoFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Parametres mis a jour.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'settings']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
/** @codeCoverageIgnore Requires live Stripe API */
|
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
|
|
|
#[Route('/mon-compte/stripe-connect', name: 'app_account_stripe_connect')]
|
|
|
|
|
public function stripeConnect(StripeService $stripeService, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if (!$this->isGranted('ROLE_ORGANIZER')) {
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!$user->getStripeAccountId()) {
|
|
|
|
|
$accountId = $stripeService->createAccountConnect($user);
|
|
|
|
|
$user->setStripeAccountId($accountId);
|
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->setStripeStatus('started');
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$link = $stripeService->createAccountLink($user->getStripeAccountId());
|
|
|
|
|
|
|
|
|
|
return $this->redirect($link);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$this->addFlash('error', 'Erreur lors de la connexion a Stripe : '.$e->getMessage());
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
/** @codeCoverageIgnore Requires live Stripe API */
|
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
|
|
|
#[Route('/mon-compte/stripe-cancel', name: 'app_account_stripe_cancel', methods: ['POST'])]
|
|
|
|
|
public function stripeCancel(StripeService $stripeService, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($this->isGranted('ROLE_ORGANIZER') && $user->getStripeAccountId()) {
|
|
|
|
|
try {
|
|
|
|
|
$stripeService->deleteAccount($user->getStripeAccountId());
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
// Account may already be deleted on Stripe side
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$user->setStripeAccountId(null);
|
|
|
|
|
$user->setStripeChargesEnabled(false);
|
|
|
|
|
$user->setStripePayoutsEnabled(false);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Compte Stripe cloture.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/stripe/connect/return', name: 'app_stripe_connect_return')]
|
|
|
|
|
public function stripeConnectReturn(): Response
|
|
|
|
|
{
|
|
|
|
|
$this->addFlash('success', 'Configuration Stripe terminee.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/stripe/connect/refresh', name: 'app_stripe_connect_refresh')]
|
|
|
|
|
public function stripeConnectRefresh(): Response
|
|
|
|
|
{
|
|
|
|
|
return $this->redirectToRoute('app_account_stripe_connect');
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[Route('/mon-compte/sous-compte/creer', name: 'app_account_create_subaccount', methods: ['POST'])]
|
|
|
|
|
public function createSubAccount(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher, MailerService $mailerService): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if (!$this->isGranted('ROLE_ORGANIZER')) {
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$plainPassword = bin2hex(random_bytes(8));
|
|
|
|
|
|
|
|
|
|
$subAccount = new User();
|
|
|
|
|
$subAccount->setFirstName(trim($request->request->getString('first_name')));
|
|
|
|
|
$subAccount->setLastName(trim($request->request->getString('last_name')));
|
|
|
|
|
$subAccount->setEmail(trim($request->request->getString('email')));
|
|
|
|
|
$subAccount->setPassword($passwordHasher->hashPassword($subAccount, $plainPassword));
|
|
|
|
|
$subAccount->setIsVerified(true);
|
|
|
|
|
$subAccount->setEmailVerifiedAt(new \DateTimeImmutable());
|
|
|
|
|
$subAccount->setParentOrganizer($user);
|
|
|
|
|
|
|
|
|
|
$permissions = $request->request->all('permissions');
|
|
|
|
|
$subAccount->setSubAccountPermissions($permissions);
|
|
|
|
|
|
|
|
|
|
$em->persist($subAccount);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$mailerService->sendEmail(
|
|
|
|
|
to: $subAccount->getEmail(),
|
|
|
|
|
subject: 'Votre sous-compte E-Ticket a ete cree',
|
|
|
|
|
content: $this->renderView('email/subaccount_created.html.twig', [
|
|
|
|
|
'firstName' => $subAccount->getFirstName(),
|
|
|
|
|
'organizerName' => $user->getCompanyName() ?? $user->getFirstName().' '.$user->getLastName(),
|
|
|
|
|
'email' => $subAccount->getEmail(),
|
|
|
|
|
'password' => $plainPassword,
|
|
|
|
|
'permissions' => $permissions,
|
|
|
|
|
]),
|
|
|
|
|
withUnsubscribe: false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', sprintf('Sous-compte %s %s cree.', $subAccount->getFirstName(), $subAccount->getLastName()));
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/sous-compte/{id}', name: 'app_account_edit_subaccount_page', methods: ['GET'])]
|
|
|
|
|
public function editSubAccountPage(User $subAccount): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('account/edit_subaccount.html.twig', [
|
|
|
|
|
'subAccount' => $subAccount,
|
Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates
- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:44:31 +01:00
|
|
|
'breadcrumbs' => [
|
2026-03-20 11:00:08 +01:00
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
|
|
|
|
['name' => 'Sous-compte', 'url' => self::BREADCRUMB_ACCOUNT['url'].'/sous-compte/'.$subAccount->getId()],
|
Add organizer pages, SEO breadcrumbs, Open Graph, homepage redesign, and infrastructure updates
- Add public organizers list page (/organisateurs) with neo-brutalist card grid, social icons, and logo display
- Add organizer detail page (/organisateur/{id}-{slug}) with company info, SIRET, email, address, social links, and events placeholder
- Add slug-based URLs with 301 redirect on wrong slug, getSlug() method on User entity
- Add "Voir les evenements" button on organizer cards linking to detail page
- Add JSON-LD BreadcrumbList to all 17 pages that were missing breadcrumbs (login, forgot_password, register_success, email_verified, legal/*, attestation/*, account/*)
- Add Open Graph meta tags (og:title, og:description, og:image, og:type, og:locale, og:site_name) in base.html.twig with automatic inheritance from title/description blocks
- Add og:image with organizer logo on detail page
- Update sitemap: add /organisateurs to sitemap-main, generate organizer detail URLs in sitemap-orgas with logo images
- Update navbar to highlight "Organisateurs" on detail pages
- Redesign homepage with hero section, marquee, stats counters, how-it-works, and CTA sections
- Add Tailwind v4 @source "../templates" directive to app.scss and admin.scss
- Migrate Flysystem from S3 to local storage (uploads/events, uploads/logos)
- Update Liip Imagine config with FormatExtensionResolver for webp conversion
- Add User entity social fields (website, facebook, instagram, twitter, tiktok), logo upload (Vich), __serialize/__unserialize for session safety
- Add account page settings tab with profile, logo upload, and social media for organizers
- Add Stripe Connect status display and sub-account management in account page
- Delete WebpExtensionSubscriber (replaced by FormatExtensionResolver)
- Add migration for social fields and logo columns
- Add deploy.yml chmod tasks for uploads directories
- Add HomeController tests (detail success, wrong slug redirect, 404 cases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:44:31 +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
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/sous-compte/{id}/modifier', name: 'app_account_edit_subaccount', methods: ['POST'])]
|
|
|
|
|
public function editSubAccount(User $subAccount, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$permissions = $request->request->all('permissions');
|
|
|
|
|
$subAccount->setSubAccountPermissions($permissions);
|
|
|
|
|
$subAccount->setFirstName(trim($request->request->getString('first_name')));
|
|
|
|
|
$subAccount->setLastName(trim($request->request->getString('last_name')));
|
|
|
|
|
$subAccount->setEmail(trim($request->request->getString('email')));
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', sprintf('Sous-compte %s %s mis a jour.', $subAccount->getFirstName(), $subAccount->getLastName()));
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/sous-compte/{id}/supprimer', name: 'app_account_delete_subaccount', methods: ['POST'])]
|
|
|
|
|
public function deleteSubAccount(User $subAccount, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$name = sprintf('%s %s', $subAccount->getFirstName(), $subAccount->getLastName());
|
|
|
|
|
$em->remove($subAccount);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', sprintf('Sous-compte %s supprime.', $name));
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[Route('/mon-compte/evenement/creer', name: 'app_account_create_event', methods: ['GET', 'POST'])]
|
2026-03-20 17:18:45 +01:00
|
|
|
public function createEvent(Request $request, EntityManagerInterface $em, EventIndexService $eventIndex): Response
|
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
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($request->isMethod('POST')) {
|
2026-03-20 19:15:22 +01:00
|
|
|
$event = new Event();
|
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
|
|
|
$event->setAccount($user);
|
2026-03-21 10:12:17 +01:00
|
|
|
$this->hydrateEventFromRequest($event, $request);
|
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
|
|
|
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
2026-03-20 17:18:45 +01:00
|
|
|
$eventIndex->indexEvent($event);
|
|
|
|
|
|
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
|
|
|
$this->addFlash('success', 'Evenement cree avec succes.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'events']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('account/create_event.html.twig', [
|
|
|
|
|
'breadcrumbs' => [
|
|
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
|
|
|
|
['name' => 'Creer un evenement', 'url' => '/mon-compte/evenement/creer'],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/modifier', name: 'app_account_edit_event', methods: ['GET', 'POST'])]
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
public function editEvent(Event $event, Request $request, EntityManagerInterface $em, EventIndexService $eventIndex, PaginatorInterface $paginator, OrderIndexService $orderIndex): Response
|
2026-03-20 17:02:56 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($request->isMethod('POST')) {
|
2026-03-21 10:12:17 +01:00
|
|
|
$this->hydrateEventFromRequest($event, $request);
|
2026-03-20 17:02:56 +01:00
|
|
|
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
2026-03-20 17:18:45 +01:00
|
|
|
$eventIndex->indexEvent($event);
|
|
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
$this->addFlash('success', 'Evenement modifie avec succes.');
|
|
|
|
|
|
2026-03-20 22:39:38 +01:00
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
|
2026-03-20 17:02:56 +01:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
$categories = $em->getRepository(Category::class)->findBy(
|
|
|
|
|
['event' => $event],
|
|
|
|
|
['position' => 'ASC'],
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-22 20:13:31 +01:00
|
|
|
$allBillets = $em->getRepository(Billet::class)->findBy(['category' => $categories], ['position' => 'ASC']);
|
|
|
|
|
|
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
|
|
|
$billets = [];
|
2026-03-22 20:13:31 +01:00
|
|
|
$billetIds = [];
|
|
|
|
|
foreach ($allBillets as $billet) {
|
|
|
|
|
$catId = $billet->getCategory()->getId();
|
|
|
|
|
$billets[$catId][] = $billet;
|
|
|
|
|
$billetIds[] = $billet->getId();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
$soldCounts = [];
|
2026-03-22 20:13:31 +01:00
|
|
|
if ($billetIds) {
|
|
|
|
|
$rows = $em->createQueryBuilder()
|
|
|
|
|
->select('IDENTITY(bo.billet) AS billetId, COUNT(bo.id) AS cnt')
|
|
|
|
|
->from(BilletOrder::class, 'bo')
|
|
|
|
|
->where('bo.billet IN (:ids)')
|
|
|
|
|
->setParameter('ids', $billetIds)
|
|
|
|
|
->groupBy('bo.billet')
|
|
|
|
|
->getQuery()
|
|
|
|
|
->getArrayResult();
|
|
|
|
|
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
$soldCounts[$row['billetId']] = (int) $row['cnt'];
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
$searchQuery = $request->query->getString('q', '');
|
|
|
|
|
$ordersQuery = '' !== $searchQuery
|
|
|
|
|
? $orderIndex->searchOrders($event->getId(), $searchQuery)
|
|
|
|
|
: $em->getRepository(BilletBuyer::class)->findBy(['event' => $event], ['createdAt' => 'DESC']);
|
|
|
|
|
$eventOrders = $paginator->paginate($ordersQuery, $request->query->getInt('page', 1), 20);
|
|
|
|
|
|
2026-03-22 20:13:31 +01:00
|
|
|
$paidEventOrders = $em->createQueryBuilder()
|
|
|
|
|
->select('o', 'i')
|
|
|
|
|
->from(BilletBuyer::class, 'o')
|
|
|
|
|
->leftJoin('o.items', 'i')
|
|
|
|
|
->where('o.event = :event')
|
|
|
|
|
->andWhere('o.status = :status')
|
|
|
|
|
->setParameter('event', $event)
|
|
|
|
|
->setParameter('status', BilletBuyer::STATUS_PAID)
|
|
|
|
|
->getQuery()
|
|
|
|
|
->getResult();
|
2026-03-21 22:36:57 +01:00
|
|
|
$eventStats = $this->computeEventStats($paidEventOrders);
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
return $this->render('account/edit_event.html.twig', [
|
|
|
|
|
'event' => $event,
|
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
|
|
|
'categories' => $categories,
|
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
|
|
|
'billets' => $billets,
|
|
|
|
|
'sold_counts' => $soldCounts,
|
|
|
|
|
'commission_rate' => $user->getCommissionRate() ?? 0,
|
|
|
|
|
'billet_design' => $em->getRepository(BilletDesign::class)->findOneBy(['event' => $event]),
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
'event_orders' => $eventOrders,
|
2026-03-21 19:14:45 +01:00
|
|
|
'invitations' => $em->getRepository(BilletBuyer::class)->findBy(['event' => $event, 'isInvitation' => true], ['createdAt' => 'DESC']),
|
2026-03-21 22:36:57 +01:00
|
|
|
'event_total_ht' => $eventStats['totalHT'] / 100,
|
|
|
|
|
'event_total_sold' => $eventStats['totalSold'],
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
'event_total_orders' => \count($paidEventOrders),
|
2026-03-21 22:36:57 +01:00
|
|
|
'billet_stats' => $eventStats['billetStats'],
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
'search_query' => $searchQuery,
|
2026-03-20 17:02:56 +01:00
|
|
|
'breadcrumbs' => [
|
|
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
2026-03-21 13:10:40 +01:00
|
|
|
['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().'/modifier'],
|
2026-03-20 17:02:56 +01:00
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[Route('/mon-compte/evenement/{id}/categorie/ajouter', name: 'app_account_event_add_category', methods: ['POST'])]
|
|
|
|
|
public function addCategory(Event $event, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$name = trim($request->request->getString('name'));
|
|
|
|
|
if ('' === $name) {
|
|
|
|
|
$this->addFlash('error', 'Le nom de la categorie est requis.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$maxPosition = $em->getRepository(Category::class)->count(['event' => $event]);
|
|
|
|
|
|
|
|
|
|
$category = new Category();
|
|
|
|
|
$category->setName($name);
|
|
|
|
|
$category->setEvent($event);
|
|
|
|
|
$category->setPosition($maxPosition);
|
|
|
|
|
|
|
|
|
|
$startAt = $request->request->getString('start_at');
|
|
|
|
|
if ('' !== $startAt) {
|
|
|
|
|
$category->setStartAt(new \DateTimeImmutable($startAt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$endAt = $request->request->getString('end_at');
|
|
|
|
|
if ('' !== $endAt) {
|
|
|
|
|
$category->setEndAt(new \DateTimeImmutable($endAt));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:13:53 +01:00
|
|
|
if ($category->getEndAt() < $category->getStartAt()) {
|
|
|
|
|
$category->setEndAt($category->getStartAt()->modify('+30 days'));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
$em->persist($category);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', sprintf('Categorie "%s" ajoutee.', $name));
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:16:52 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/modifier', name: 'app_account_event_edit_category', methods: ['GET', 'POST'])]
|
|
|
|
|
public function editCategory(Event $event, int $categoryId, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$category = $em->getRepository(Category::class)->find($categoryId);
|
|
|
|
|
if (!$category || $category->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($request->isMethod('POST')) {
|
|
|
|
|
$category->setName(trim($request->request->getString('name')));
|
|
|
|
|
|
|
|
|
|
$startAt = $request->request->getString('start_at');
|
|
|
|
|
if ('' !== $startAt) {
|
|
|
|
|
$category->setStartAt(new \DateTimeImmutable($startAt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$endAt = $request->request->getString('end_at');
|
|
|
|
|
if ('' !== $endAt) {
|
|
|
|
|
$category->setEndAt(new \DateTimeImmutable($endAt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($category->getEndAt() < $category->getStartAt()) {
|
|
|
|
|
$category->setEndAt($category->getStartAt()->modify('+30 days'));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:35:42 +01:00
|
|
|
$category->setIsHidden($request->request->getBoolean('is_hidden'));
|
|
|
|
|
|
2026-03-20 23:16:52 +01:00
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Categorie modifiee.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('account/edit_category.html.twig', [
|
|
|
|
|
'event' => $event,
|
|
|
|
|
'category' => $category,
|
|
|
|
|
'breadcrumbs' => [
|
|
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
2026-03-21 13:11:54 +01:00
|
|
|
['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
|
2026-03-20 23:16:52 +01:00
|
|
|
['name' => $category->getName(), 'url' => ''],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/supprimer', name: 'app_account_event_delete_category', methods: ['POST'])]
|
|
|
|
|
public function deleteCategory(Event $event, int $categoryId, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$category = $em->getRepository(Category::class)->find($categoryId);
|
|
|
|
|
if ($category && $category->getEvent()->getId() === $event->getId()) {
|
|
|
|
|
$em->remove($category);
|
|
|
|
|
$em->flush();
|
|
|
|
|
$this->addFlash('success', 'Categorie supprimee.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/categorie/reorder', name: 'app_account_event_reorder_categories', methods: ['POST'])]
|
|
|
|
|
public function reorderCategories(Event $event, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = json_decode($request->getContent(), true);
|
|
|
|
|
if (\is_array($order)) {
|
|
|
|
|
foreach ($order as $position => $categoryId) {
|
|
|
|
|
$category = $em->getRepository(Category::class)->find($categoryId);
|
|
|
|
|
if ($category && $category->getEvent()->getId() === $event->getId()) {
|
|
|
|
|
$category->setPosition($position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$em->flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->json(['success' => true]);
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/billet/ajouter', name: 'app_account_event_add_billet', methods: ['GET', 'POST'])]
|
|
|
|
|
public function addBillet(Event $event, int $categoryId, Request $request, EntityManagerInterface $em, StripeService $stripeService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$category = $em->getRepository(Category::class)->find($categoryId);
|
|
|
|
|
if (!$category || $category->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($request->isMethod('POST')) {
|
|
|
|
|
$billet = new Billet();
|
|
|
|
|
$billet->setCategory($category);
|
|
|
|
|
$billet->setPosition($em->getRepository(Billet::class)->count(['category' => $category]));
|
2026-03-21 13:10:40 +01:00
|
|
|
$this->hydrateBilletFromRequest($billet, $request);
|
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
|
|
|
|
|
|
|
|
$em->persist($billet);
|
2026-03-21 13:13:35 +01:00
|
|
|
$this->syncBilletToStripe($billet, $user, $stripeService);
|
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
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Billet ajoute avec succes.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('account/add_billet.html.twig', [
|
|
|
|
|
'event' => $event,
|
|
|
|
|
'category' => $category,
|
|
|
|
|
'breadcrumbs' => [
|
|
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
2026-03-21 13:11:54 +01:00
|
|
|
['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
|
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
|
|
|
['name' => 'Ajouter un billet'],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/billet/{billetId}/modifier', name: 'app_account_event_edit_billet', methods: ['GET', 'POST'])]
|
|
|
|
|
public function editBillet(Event $event, int $billetId, Request $request, EntityManagerInterface $em, StripeService $stripeService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$billet = $em->getRepository(Billet::class)->find($billetId);
|
|
|
|
|
if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($request->isMethod('POST')) {
|
2026-03-21 13:10:40 +01:00
|
|
|
$this->hydrateBilletFromRequest($billet, $request);
|
2026-03-21 13:13:35 +01:00
|
|
|
$this->syncBilletToStripe($billet, $user, $stripeService);
|
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
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Billet modifie avec succes.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('account/edit_billet.html.twig', [
|
|
|
|
|
'event' => $event,
|
|
|
|
|
'billet' => $billet,
|
|
|
|
|
'breadcrumbs' => [
|
|
|
|
|
self::BREADCRUMB_HOME,
|
|
|
|
|
self::BREADCRUMB_ACCOUNT,
|
2026-03-21 13:11:54 +01:00
|
|
|
['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
|
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
|
|
|
['name' => 'Modifier un billet'],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/billet/{billetId}/supprimer', name: 'app_account_event_delete_billet', methods: ['POST'])]
|
|
|
|
|
public function deleteBillet(Event $event, int $billetId, EntityManagerInterface $em, StripeService $stripeService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$billet = $em->getRepository(Billet::class)->find($billetId);
|
|
|
|
|
if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 13:30:38 +01:00
|
|
|
$this->deleteBilletFromStripe($billet, $user, $stripeService);
|
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
|
|
|
|
|
|
|
|
$em->remove($billet);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Billet supprime avec succes.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/billet/reorder', name: 'app_account_event_reorder_billets', methods: ['POST'])]
|
|
|
|
|
public function reorderBillets(Event $event, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = json_decode($request->getContent(), true);
|
|
|
|
|
if (\is_array($order)) {
|
|
|
|
|
foreach ($order as $position => $billetId) {
|
|
|
|
|
$billet = $em->getRepository(Billet::class)->find($billetId);
|
|
|
|
|
if ($billet && $billet->getCategory()->getEvent()->getId() === $event->getId()) {
|
|
|
|
|
$billet->setPosition($position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$em->flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->json(['success' => true]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 17:27:18 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/invitation', name: 'app_account_event_create_invitation', methods: ['POST'])]
|
|
|
|
|
public function createInvitation(Event $event, Request $request, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$firstName = trim($request->request->getString('first_name'));
|
|
|
|
|
$lastName = trim($request->request->getString('last_name'));
|
|
|
|
|
$email = trim($request->request->getString('email'));
|
2026-03-21 17:30:18 +01:00
|
|
|
$items = $request->request->all('items');
|
2026-03-21 17:27:18 +01:00
|
|
|
|
2026-03-21 17:30:18 +01:00
|
|
|
if ('' === $firstName || '' === $lastName || '' === $email || 0 === \count($items)) {
|
2026-03-21 17:27:18 +01:00
|
|
|
$this->addFlash('error', 'Tous les champs sont requis.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$count = $em->getRepository(BilletBuyer::class)->count([]) + 1;
|
|
|
|
|
|
|
|
|
|
$order = new BilletBuyer();
|
|
|
|
|
$order->setEvent($event);
|
|
|
|
|
$order->setFirstName($firstName);
|
|
|
|
|
$order->setLastName($lastName);
|
|
|
|
|
$order->setEmail($email);
|
|
|
|
|
$order->setOrderNumber(date('Y-m-d').'-'.$count);
|
|
|
|
|
$order->setTotalHT(0);
|
2026-03-21 19:14:45 +01:00
|
|
|
$order->setIsInvitation(true);
|
2026-03-21 17:27:18 +01:00
|
|
|
|
2026-03-21 17:30:18 +01:00
|
|
|
foreach ($items as $itemData) {
|
|
|
|
|
$billetId = (int) ($itemData['billet_id'] ?? 0);
|
|
|
|
|
$qty = max(1, (int) ($itemData['quantity'] ?? 1));
|
|
|
|
|
|
|
|
|
|
$billet = $em->getRepository(Billet::class)->find($billetId);
|
|
|
|
|
if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$item = new BilletBuyerItem();
|
|
|
|
|
$item->setBillet($billet);
|
|
|
|
|
$item->setBilletName($billet->getName());
|
|
|
|
|
$item->setQuantity($qty);
|
|
|
|
|
$item->setUnitPriceHT(0);
|
|
|
|
|
$order->addItem($item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($order->getItems()->isEmpty()) {
|
|
|
|
|
$this->addFlash('error', 'Aucun billet valide selectionne.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
|
|
|
|
|
}
|
2026-03-21 17:27:18 +01:00
|
|
|
|
|
|
|
|
$em->persist($order);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$billetOrderService->generateOrderTickets($order);
|
|
|
|
|
|
|
|
|
|
$tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
|
|
|
|
|
foreach ($tickets as $ticket) {
|
|
|
|
|
$ticket->setIsInvitation(true);
|
|
|
|
|
}
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$billetOrderService->generateAndSendTickets($order);
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Invitation envoyee a '.$email.'.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 19:11:15 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/invitation/{orderId}/renvoyer', name: 'app_account_event_resend_invitation', methods: ['POST'])]
|
|
|
|
|
public function resendInvitation(Event $event, int $orderId, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = $em->getRepository(BilletBuyer::class)->find($orderId);
|
|
|
|
|
if (!$order || $order->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$billetOrderService->generateAndSendTickets($order);
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Invitation renvoyee a '.$order->getEmail().'.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
|
|
|
|
|
}
|
|
|
|
|
|
Add stats tab with orders list, search, cancel and refund actions
- OrderIndexService: Meilisearch index order_event_{id} for order search
- Stats tab: 4 KPI cards (orders, tickets sold, CA HT, total percu)
- Orders list with KnpPaginator, search bar via Meilisearch
- Each order shows: number, status, date, buyer, items, total, payment
- Cancel order: sets status cancelled, invalidates all tickets
- Refund order: Stripe refund on connected account, sets status refunded,
invalidates all tickets
- Orders indexed in Meilisearch after payment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:19:58 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/commande/{orderId}/annuler', name: 'app_account_event_cancel_order', methods: ['POST'])]
|
|
|
|
|
public function cancelOrder(Event $event, int $orderId, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = $em->getRepository(BilletBuyer::class)->find($orderId);
|
|
|
|
|
if (!$order || $order->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order->setStatus(BilletBuyer::STATUS_CANCELLED);
|
|
|
|
|
|
|
|
|
|
$tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
|
|
|
|
|
foreach ($tickets as $ticket) {
|
|
|
|
|
$ticket->setState(BilletOrder::STATE_INVALID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Commande '.$order->getOrderNumber().' annulee.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @codeCoverageIgnore Requires live Stripe API
|
|
|
|
|
*/
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/commande/{orderId}/rembourser', name: 'app_account_event_refund_order', methods: ['POST'])]
|
|
|
|
|
public function refundOrder(Event $event, int $orderId, EntityManagerInterface $em, StripeService $stripeService): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = $em->getRepository(BilletBuyer::class)->find($orderId);
|
|
|
|
|
if (!$order || $order->getEvent()->getId() !== $event->getId()) {
|
|
|
|
|
throw $this->createNotFoundException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($order->getStripeSessionId() && $user->getStripeAccountId()) {
|
|
|
|
|
try {
|
|
|
|
|
$stripeService->getClient()->refunds->create([
|
|
|
|
|
'payment_intent' => $order->getStripeSessionId(),
|
|
|
|
|
], ['stripe_account' => $user->getStripeAccountId()]);
|
|
|
|
|
} catch (\Exception) {
|
|
|
|
|
// Stripe failure is non-blocking
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order->setStatus(BilletBuyer::STATUS_REFUNDED);
|
|
|
|
|
|
|
|
|
|
$tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
|
|
|
|
|
foreach ($tickets as $ticket) {
|
|
|
|
|
$ticket->setState(BilletOrder::STATE_INVALID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Commande '.$order->getOrderNumber().' remboursee.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[Route('/mon-compte/evenement/{id}/billet-preview', name: 'app_account_event_billet_preview', methods: ['GET'])]
|
|
|
|
|
public function billetPreview(Event $event, Request $request): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 13:13:35 +01:00
|
|
|
return $this->render('account/billet_preview.html.twig', [
|
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
|
|
|
'event' => $event,
|
|
|
|
|
'user' => $user,
|
|
|
|
|
'bg_color' => '#ffffff',
|
|
|
|
|
'text_color' => '#111111',
|
|
|
|
|
'accent_color' => $request->query->getString('accent_color', '#4f46e5'),
|
|
|
|
|
'show_logo' => true,
|
|
|
|
|
'show_invitation' => true,
|
|
|
|
|
'invitation_title' => $request->query->getString('invitation_title', 'Invitation'),
|
|
|
|
|
'invitation_color' => $request->query->getString('invitation_color', '#d4a017'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/billet-design', name: 'app_account_event_save_billet_design', methods: ['POST'])]
|
|
|
|
|
public function saveBilletDesign(Event $event, Request $request, EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$design = $em->getRepository(BilletDesign::class)->findOneBy(['event' => $event]);
|
|
|
|
|
if (!$design) {
|
|
|
|
|
$design = new BilletDesign();
|
|
|
|
|
$design->setEvent($event);
|
|
|
|
|
$em->persist($design);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$design->setAccentColor($request->request->getString('accent_color', '#4f46e5'));
|
|
|
|
|
$design->setInvitationTitle($request->request->getString('invitation_title', 'Invitation'));
|
|
|
|
|
$design->setInvitationColor($request->request->getString('invitation_color', '#d4a017'));
|
|
|
|
|
$design->setUpdatedAt(new \DateTimeImmutable());
|
|
|
|
|
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
return $this->json(['success' => true]);
|
|
|
|
|
}
|
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
|
|
|
|
2026-03-20 17:42:08 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/en-ligne', name: 'app_account_toggle_event_online', methods: ['POST'])]
|
2026-03-20 19:15:22 +01:00
|
|
|
public function toggleEventOnline(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
|
2026-03-20 17:42:08 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:47:31 +01:00
|
|
|
if (!$event->isOnline() && (!$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled())) {
|
|
|
|
|
$this->addFlash('error', 'Configuration Stripe requise pour mettre un evenement en ligne.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:42:08 +01:00
|
|
|
$event->setIsOnline(!$event->isOnline());
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$eventIndex->indexEvent($event);
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', $event->isOnline() ? 'Evenement mis en ligne.' : 'Evenement passe hors ligne.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[Route('/mon-compte/evenement/{id}/secret', name: 'app_account_toggle_event_secret', methods: ['POST'])]
|
2026-03-20 19:15:22 +01:00
|
|
|
public function toggleEventSecret(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
|
2026-03-20 17:42:08 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$event->setIsSecret(!$event->isSecret());
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$eventIndex->indexEvent($event);
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', $event->isSecret() ? 'Evenement marque comme secret.' : 'Evenement rendu public.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
#[Route('/mon-compte/evenement/{id}/supprimer', name: 'app_account_delete_event', methods: ['POST'])]
|
2026-03-20 19:15:22 +01:00
|
|
|
public function deleteEvent(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
|
2026-03-20 17:02:56 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
|
|
|
|
|
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
if ($event->getAccount()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:18:45 +01:00
|
|
|
$eventIndex->removeEvent($event);
|
|
|
|
|
|
2026-03-20 17:02:56 +01:00
|
|
|
$em->remove($event);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', 'Evenement supprime.');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'events']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
/** @codeCoverageIgnore Test helper, not used in production */
|
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
|
|
|
#[Route('/mon-compte/test-payout', name: 'app_account_test_payout', methods: ['POST'])]
|
|
|
|
|
public function testPayout(EntityManagerInterface $em): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if (!$this->isGranted('ROLE_ORGANIZER') || !$user->getStripeAccountId()) {
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payout = new Payout();
|
|
|
|
|
$payout->setOrganizer($user);
|
|
|
|
|
$payout->setStripePayoutId('po_test_'.bin2hex(random_bytes(8)));
|
|
|
|
|
$payout->setStatus('paid');
|
|
|
|
|
$payout->setAmount(random_int(1000, 50000));
|
|
|
|
|
$payout->setCurrency('eur');
|
|
|
|
|
$payout->setDestination('ba_test_bank');
|
|
|
|
|
$payout->setStripeAccountId($user->getStripeAccountId());
|
|
|
|
|
$payout->setArrivalDate(new \DateTimeImmutable('+2 days'));
|
|
|
|
|
|
|
|
|
|
$em->persist($payout);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$this->addFlash('success', sprintf('Payout test cree : %s (%.2f EUR)', $payout->getStripePayoutId(), $payout->getAmountDecimal()));
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account', ['tab' => 'payouts']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
/** @codeCoverageIgnore Requires live Stripe API */
|
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
|
|
|
#[Route('/mon-compte/stripe-dashboard', name: 'app_account_stripe_dashboard')]
|
|
|
|
|
public function stripeDashboard(StripeService $stripeService): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if (!$this->isGranted('ROLE_ORGANIZER') || !$user->getStripeAccountId()) {
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$link = $stripeService->createLoginLink($user->getStripeAccountId());
|
|
|
|
|
|
|
|
|
|
return $this->redirect($link);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$this->addFlash('error', 'Erreur Stripe : '.$e->getMessage());
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('app_account');
|
|
|
|
|
}
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +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
|
|
|
|
2026-03-20 00:05:17 +01:00
|
|
|
/** @codeCoverageIgnore Generates PDF with dompdf */
|
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
|
|
|
#[Route('/mon-compte/payout/{id}/attestation', name: 'app_account_payout_pdf')]
|
|
|
|
|
public function payoutPdf(Payout $payout, PayoutPdfService $pdfService): Response
|
|
|
|
|
{
|
|
|
|
|
/** @var User $user */
|
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
|
|
if ($payout->getOrganizer()->getId() !== $user->getId()) {
|
|
|
|
|
throw $this->createAccessDeniedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Response($pdfService->generate($payout), 200, [
|
|
|
|
|
'Content-Type' => 'application/pdf',
|
|
|
|
|
'Content-Disposition' => 'inline; filename="attestation_'.$payout->getStripePayoutId().'.pdf"',
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-03-21 10:12:17 +01:00
|
|
|
|
2026-03-21 13:30:38 +01:00
|
|
|
/**
|
|
|
|
|
* @codeCoverageIgnore Requires live Stripe API
|
|
|
|
|
*/
|
|
|
|
|
private function deleteBilletFromStripe(Billet $billet, User $user, StripeService $stripeService): void
|
|
|
|
|
{
|
|
|
|
|
if ($billet->getStripeProductId() && $user->getStripeAccountId()) {
|
|
|
|
|
try {
|
|
|
|
|
$stripeService->deleteProduct($billet->getStripeProductId(), $user->getStripeAccountId());
|
|
|
|
|
} catch (\Exception) {
|
|
|
|
|
// Stripe failure is non-blocking
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @codeCoverageIgnore Requires live Stripe API
|
|
|
|
|
*/
|
2026-03-21 13:13:35 +01:00
|
|
|
private function syncBilletToStripe(Billet $billet, User $user, StripeService $stripeService): void
|
|
|
|
|
{
|
|
|
|
|
if (!$user->getStripeAccountId()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if ($billet->getStripeProductId()) {
|
|
|
|
|
$stripeService->updateProduct($billet, $user->getStripeAccountId());
|
|
|
|
|
} else {
|
|
|
|
|
$productId = $stripeService->createProduct($billet, $user->getStripeAccountId());
|
|
|
|
|
$billet->setStripeProductId($productId);
|
|
|
|
|
}
|
|
|
|
|
} catch (\Exception) {
|
|
|
|
|
// Stripe failure is non-blocking
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 22:36:57 +01:00
|
|
|
/**
|
|
|
|
|
* @param list<BilletBuyer> $paidOrders
|
|
|
|
|
*
|
|
|
|
|
* @return array{totalHT: int, totalSold: int, billetStats: array<int, array{name: string, sold: int, revenue: int}>}
|
|
|
|
|
*/
|
|
|
|
|
private function computeEventStats(array $paidOrders): array
|
|
|
|
|
{
|
|
|
|
|
$totalHT = 0;
|
|
|
|
|
$totalSold = 0;
|
|
|
|
|
$billetStats = [];
|
|
|
|
|
|
|
|
|
|
foreach ($paidOrders as $order) {
|
|
|
|
|
$totalHT += $order->getTotalHT();
|
|
|
|
|
foreach ($order->getItems() as $item) {
|
|
|
|
|
$totalSold += $item->getQuantity();
|
|
|
|
|
$billetId = $item->getBillet()?->getId();
|
|
|
|
|
if (!$billetId) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!isset($billetStats[$billetId])) {
|
|
|
|
|
$billetStats[$billetId] = ['name' => $item->getBilletName(), 'sold' => 0, 'revenue' => 0];
|
|
|
|
|
}
|
|
|
|
|
$billetStats[$billetId]['sold'] += $item->getQuantity();
|
|
|
|
|
$billetStats[$billetId]['revenue'] += $item->getLineTotalHT();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['totalHT' => $totalHT, 'totalSold' => $totalSold, 'billetStats' => $billetStats];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 10:12:17 +01:00
|
|
|
private function hydrateEventFromRequest(Event $event, Request $request): void
|
|
|
|
|
{
|
|
|
|
|
$event->setTitle(trim($request->request->getString('title')));
|
|
|
|
|
$event->setDescription(trim($request->request->getString('description')) ?: null);
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable($request->request->getString('start_at')));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable($request->request->getString('end_at')));
|
|
|
|
|
$event->setAddress(trim($request->request->getString('address')));
|
|
|
|
|
$event->setZipcode(trim($request->request->getString('zipcode')));
|
|
|
|
|
$event->setCity(trim($request->request->getString('city')));
|
|
|
|
|
|
|
|
|
|
$pictureFile = $request->files->get('event_main_picture');
|
|
|
|
|
if ($pictureFile) {
|
|
|
|
|
$event->setEventMainPictureFile($pictureFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-21 13:10:40 +01:00
|
|
|
|
|
|
|
|
private function hydrateBilletFromRequest(Billet $billet, Request $request): void
|
|
|
|
|
{
|
|
|
|
|
$billet->setName(trim($request->request->getString('name')));
|
|
|
|
|
$billet->setPriceHT((int) round((float) $request->request->getString('price_ht') * 100));
|
|
|
|
|
$qty = $request->request->getString('quantity');
|
|
|
|
|
$billet->setQuantity('' === $qty ? null : (int) $qty);
|
|
|
|
|
$billet->setIsGeneratedBillet($request->request->getBoolean('is_generated_billet'));
|
|
|
|
|
$billet->setHasDefinedExit($request->request->getBoolean('has_defined_exit'));
|
|
|
|
|
$billet->setNotBuyable($request->request->getBoolean('not_buyable'));
|
|
|
|
|
$billet->setType($request->request->getString('type', 'billet'));
|
|
|
|
|
$billet->setDescription(trim($request->request->getString('description')) ?: null);
|
|
|
|
|
|
|
|
|
|
$pictureFile = $request->files->get('picture');
|
|
|
|
|
if ($pictureFile) {
|
|
|
|
|
$billet->setPictureFile($pictureFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
Add application source code, configs and assets
- Controllers, Entity, Repository, Services, Twig extensions
- Templates (account, emails, home, legal, security, unsubscribe)
- Symfony config updates (bundles, security, framework, services)
- Vite + Bun setup with PostCSS
- Caddy config, CLAUDE.md, README
- Update .gitignore (node_modules, .idea, cert)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:16:01 +01:00
|
|
|
}
|