Add homepage, tarifs, legal pages, navbar, footer and full test coverage

- Homepage: hero, how it works (buyer/organizer), features, CTA
- Tarifs: 3 plans (Gratuit, Basique 10€, Sur-mesure), JSON-LD Product
- Legal pages: mentions legales, CGU (tabs buyer/organizer), CGV, RGPD, cookies, hosting
- Navbar: neubrutalism style, logo liip, mobile menu, SEO attributes
- Footer: contact, description, legal links, tarifs
- Sitemap: add /tarifs and /sitemap-orgas-{page}.xml
- Liip Imagine: remove S3, webp format on all filters
- Tests: full coverage for all controllers, services, repositories
- Fix CSP: replace inline onclick with data-tab JS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-19 00:01:58 +01:00
parent 4990e5cfe2
commit af8bbc24dc
30 changed files with 1631 additions and 39 deletions

1
.gitignore vendored
View File

@@ -16,6 +16,7 @@
node_modules/
.idea/
cert/
/public/media/
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php

View File

@@ -55,6 +55,12 @@ clear_prod: ## Clear le cache Symfony et le pool opcache en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console cache:clear --env=prod
docker compose -f docker-compose-prod.yml exec php php bin/console cache:pool:clear --all --env=prod
purge_liip_dev: ## Purge le cache Liip Imagine en dev via Docker
docker compose -f docker-compose-dev.yml exec php php bin/console liip:imagine:cache:remove
purge_liip_prod: ## Purge le cache Liip Imagine en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console liip:imagine:cache:remove --env=prod
## —— Maintenance ——————————————————————————————————
maintenance_on: ## Active le mode maintenance
touch public/.update

View File

@@ -1 +1,30 @@
import "./app.scss"
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('mobile-menu-btn')
const menu = document.getElementById('mobile-menu')
const iconOpen = document.getElementById('menu-icon-open')
const iconClose = document.getElementById('menu-icon-close')
if (btn && menu) {
btn.addEventListener('click', () => {
const isOpen = !menu.classList.contains('hidden')
menu.classList.toggle('hidden')
iconOpen.classList.toggle('hidden')
iconClose.classList.toggle('hidden')
btn.setAttribute('aria-expanded', String(!isOpen))
})
}
document.querySelectorAll('[data-tab]').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.dataset.tab
document.querySelectorAll('[data-tab]').forEach(b => {
const isActive = b.dataset.tab === targetId
b.style.backgroundColor = isActive ? '#111827' : 'white'
b.style.color = isActive ? 'white' : '#111827'
document.getElementById(b.dataset.tab).style.display = isActive ? 'block' : 'none'
})
})
})
})

View File

@@ -1,3 +1,2 @@
@use "tailwindcss";
@import "tailwindcss";
@import 'https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap';

View File

@@ -3,39 +3,32 @@ liip_imagine:
twig:
mode: lazy
loaders:
flysystem_loader:
flysystem:
filesystem_service: default.storage
data_loader: flysystem_loader
resolvers:
flysystem_resolver:
flysystem:
filesystem_service: default.storage
root_url: '%env(S3_ENDPOINT)%/%env(S3_BUCKET)%'
cache_prefix: cache
cache: flysystem_resolver
webp:
generate: true
quality: 80
filter_sets:
navbar_logo:
quality: 85
format: webp
filters:
thumbnail: { size: [200, 72], mode: inset }
thumbnail:
quality: 80
format: webp
filters:
thumbnail: { size: [300, 300], mode: inset }
background: { size: [300, 300], position: center, color: '#ffffff' }
medium:
quality: 85
format: webp
filters:
thumbnail: { size: [600, 600], mode: inset }
large:
quality: 90
format: webp
filters:
thumbnail: { size: [1200, 1200], mode: inset }

1
public/logo.jpg Normal file
View File

@@ -0,0 +1 @@
fake-image

View File

@@ -17,4 +17,15 @@ class HomeController extends AbstractController
],
]);
}
#[Route('/tarifs', name: 'app_tarifs')]
public function tarifs(): Response
{
return $this->render('home/tarifs.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Tarifs', 'url' => '/tarifs'],
],
]);
}
}

View File

@@ -21,12 +21,20 @@ class SitemapController extends AbstractController
['loc' => $this->generateUrl('app_sitemap_main', [], UrlGeneratorInterface::ABSOLUTE_URL)],
];
$orgaPages = max(1, (int) ceil(0 / self::MAX_URLS_PER_SITEMAP));
for ($i = 1; $i <= $eventPages; ++$i) {
$sitemaps[] = [
'loc' => $this->generateUrl('app_sitemap_events', ['page' => $i], UrlGeneratorInterface::ABSOLUTE_URL),
];
}
for ($i = 1; $i <= $orgaPages; ++$i) {
$sitemaps[] = [
'loc' => $this->generateUrl('app_sitemap_orgas', ['page' => $i], UrlGeneratorInterface::ABSOLUTE_URL),
];
}
return new Response(
$this->renderView('sitemap/index.xml.twig', ['sitemaps' => $sitemaps]),
200,
@@ -48,6 +56,11 @@ class SitemapController extends AbstractController
'changefreq' => 'weekly',
'priority' => '0.5',
],
[
'loc' => $this->generateUrl('app_tarifs', [], UrlGeneratorInterface::ABSOLUTE_URL),
'changefreq' => 'monthly',
'priority' => '0.7',
],
];
return new Response(
@@ -68,4 +81,16 @@ class SitemapController extends AbstractController
['Content-Type' => self::CONTENT_TYPE_XML],
);
}
#[Route('/sitemap-orgas-{page}.xml', name: 'app_sitemap_orgas', requirements: ['page' => '\d+'], methods: ['GET'])]
public function orgas(int $page = 1): Response
{
$urls = [];
return new Response(
$this->renderView('sitemap/urlset.xml.twig', ['urls' => $urls]),
200,
['Content-Type' => self::CONTENT_TYPE_XML],
);
}
}

View File

@@ -68,7 +68,101 @@
{{ vite_asset('app.js') }}
{% endblock %}
</head>
<body>
<body class="min-h-screen flex flex-col bg-[#fbfbfb] text-[#111827]">
<header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900">
<nav class="mx-auto px-4 lg:px-8" role="navigation" aria-label="Navigation principale" itemscope itemtype="https://schema.org/SiteNavigationElement">
<div class="flex justify-between items-center h-20">
<div class="flex-shrink-0">
<a href="{{ path('app_home') }}" class="flex items-center group" aria-label="E-Ticket - Retour a l'accueil">
<div class="relative p-2 border-2 border-gray-900 shadow-[4px_4px_0px_rgba(0,0,0,1)] group-hover:translate-x-1 group-hover:translate-y-1 group-hover:shadow-none transition-all">
<img class="h-8 w-auto" src="{{ 'logo.png' | imagine_filter('navbar_logo') }}" alt="E-Ticket" loading="eager">
</div>
<span class="ml-4 text-2xl font-black uppercase tracking-tighter italic">E-Ticket</span>
</a>
</div>
<div class="hidden lg:flex items-center space-x-1">
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]"><span itemprop="name">Accueil</span></a>
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Evenements</span></a>
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Contact</span></a>
</div>
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
<div class="flex items-center gap-2">
{% if app.user %}
<a href="{{ path('app_account') }}" class="p-2 border-2 border-gray-900 bg-white text-gray-900 hover:bg-gray-900 hover:text-white transition-all flex items-center justify-center" aria-label="Mon espace">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
</a>
{% else %}
<a href="{{ path('app_login') }}" class="p-2 border-2 border-gray-900 bg-white text-gray-900 hover:bg-gray-900 hover:text-white transition-all flex items-center justify-center" aria-label="Connexion">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
</a>
{% endif %}
<button id="mobile-menu-btn" class="lg:hidden p-2 border-2 border-gray-900 bg-yellow-400" aria-label="Ouvrir le menu de navigation" aria-expanded="false" aria-controls="mobile-menu">
<svg id="menu-icon-open" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M4 6h16M4 12h16M4 18h16"/></svg>
<svg id="menu-icon-close" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
</nav>
<div id="mobile-menu" class="hidden lg:hidden border-t-4 border-gray-900 bg-white" role="menu">
<div class="p-4 space-y-2 uppercase font-black italic">
<a href="{{ path('app_home') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Accueil</a>
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Evenements</a>
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Contact</a>
{% if app.user %}
<a href="{{ path('app_account') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Mon espace</a>
{% else %}
<a href="{{ path('app_login') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Connexion</a>
{% endif %}
</div>
</div>
</header>
<main class="flex-1">
{% block body %}{% endblock %}
</main>
<footer class="bg-yellow-400 border-t-8 border-gray-900 text-gray-900 mt-auto">
<div style="max-width:80rem;margin:0 auto;padding:3rem 1rem;">
<div style="display:flex;flex-wrap:wrap;gap:3rem;padding-bottom:3rem;margin-bottom:3rem;border-bottom:4px solid #111827;">
<div style="flex:1;min-width:280px;">
<h3 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:1rem;">Nous Contacter</h3>
<p class="font-bold text-lg" style="line-height:1.4;">42 RUE DE SAINT-QUENTIN<br>02800 BEAUTOR, FRANCE</p>
<a href="mailto:contact@e-cosplay.fr" style="display:inline-block;margin-top:1rem;padding:0.5rem 1rem;" class="bg-gray-900 text-white font-black uppercase text-sm hover:bg-indigo-600 transition-colors">
contact@e-cosplay.fr
</a>
</div>
<div style="flex:1;min-width:280px;">
<h3 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:1rem;">E-Ticket</h3>
<p class="font-bold text-gray-800 italic" style="line-height:1.5;">
E-Ticket est une plateforme de billetterie destinee aux associations
pour la vente de tickets evenementiels, la reservation de tables,
l'organisation de brocantes et le vote en ligne.
</p>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1.5rem;">
<div>
<p class="font-black uppercase text-sm">&copy; {{ "now"|date("Y") }} E-COSPLAY.</p>
<p style="font-size:10px;" class="font-bold opacity-70">RNA N&deg;W022006988</p>
</div>
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
<a href="{{ path('app_mentions_legales') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Mentions Legales</a>
<a href="{{ path('app_cookies') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Politique de Cookies</a>
<a href="{{ path('app_cgu') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">CGU</a>
<a href="{{ path('app_cgv') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">CGV</a>
<a href="{{ path('app_rgpd') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Politique RGPD</a>
<a href="{{ path('app_hosting') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Hebergement</a>
<a href="{{ path('app_tarifs') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Tarifs</a>
</div>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -1,7 +1,110 @@
{% extends 'base.html.twig' %}
{% block title %}Accueil - E-Ticket{% endblock %}
{% block title %}E-Ticket - Billetterie en ligne pour associations{% endblock %}
{% block description %}E-Ticket, plateforme de billetterie en ligne pour associations : vente de tickets, reservation de tables, brocantes et vote en ligne.{% endblock %}
{% block body %}
<section style="background:#111827;color:white;padding:5rem 1rem;text-align:center;">
<div style="max-width:50rem;margin:0 auto;">
<h1 class="text-4xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">
La billetterie pensee pour les <span style="color:#fabf04;">associations</span>
</h1>
<p class="text-lg font-bold" style="opacity:0.8;margin-bottom:2rem;">
Vendez vos billets, gerez vos evenements et simplifiez votre organisation. Simple, securise, transparent.
</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;">
<a href="{{ path('app_register') }}" style="padding:1rem 2rem;border:3px solid white;background:#fabf04;color:#111827;" class="font-black uppercase text-sm hover:shadow-none transition-all shadow-[4px_4px_0px_rgba(255,255,255,0.3)]">Creer mon evenement</a>
<a href="#acheteur" style="padding:1rem 2rem;border:3px solid white;background:transparent;color:white;" class="font-black uppercase text-sm hover:bg-white hover:text-gray-900 transition-all">Trouver un evenement</a>
</div>
</div>
</section>
<section style="max-width:70rem;margin:0 auto;padding:4rem 1rem;">
<div style="text-align:center;margin-bottom:3rem;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;">Comment ca marche ?</h2>
</div>
<div id="acheteur" style="margin-bottom:4rem;">
<h3 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;color:#4f46e5;">Pour les acheteurs</h3>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#fabf04;">1</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Choisissez</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Parcourez les evenements et selectionnez vos billets.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#4f46e5;">2</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Payez en ligne</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Paiement securise par carte bancaire via Stripe.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#ec4899;">3</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Recevez votre billet</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Billet electronique avec QR Code envoye par email signe.</p>
</div>
</div>
</div>
<div>
<h3 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;color:#ec4899;">Pour les organisateurs</h3>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#fabf04;">1</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Creez votre evenement</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Configurez vos billets, tarifs et nombre de places en quelques minutes.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#4f46e5;">2</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Vendez en ligne</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Partagez le lien de votre evenement. Les paiements sont automatiques.</p>
</div>
<div style="flex:1;min-width:200px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-4xl font-black" style="color:#ec4899;">3</p>
<p class="font-black uppercase text-sm" style="margin-top:0.5rem;">Scannez a l'entree</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Validez les billets QR Code depuis n'importe quel smartphone.</p>
</div>
</div>
</div>
</section>
<section style="background:#fabf04;border-top:4px solid #111827;border-bottom:4px solid #111827;padding:4rem 1rem;">
<div style="max-width:70rem;margin:0 auto;">
<div style="text-align:center;margin-bottom:3rem;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic">Pourquoi E-Ticket ?</h2>
</div>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#128274;</p>
<p class="font-black uppercase text-sm">Securise</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Paiement Stripe, HTTPS, emails signes S/MIME, protection Cloudflare.</p>
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#128176;</p>
<p class="font-black uppercase text-sm">Transparent</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Commission claire a 3%, negociable. Aucun frais cache.</p>
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#9889;</p>
<p class="font-black uppercase text-sm">Simple</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Creez un evenement en 5 minutes. Aucune competence technique requise.</p>
</div>
<div style="flex:1;min-width:220px;border:4px solid #111827;padding:1.5rem;background:white;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<p class="text-2xl font-black" style="margin-bottom:0.5rem;">&#127915;</p>
<p class="font-black uppercase text-sm">Fait pour les assos</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.25rem;">Billets, brocantes, tables, votes. Tout ce dont votre association a besoin.</p>
</div>
</div>
</div>
</section>
<section style="max-width:50rem;margin:0 auto;padding:4rem 1rem;text-align:center;">
<h2 class="text-3xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">Pret a lancer votre evenement ?</h2>
<p class="text-lg font-bold text-gray-600" style="margin-bottom:2rem;">Rejoignez E-Ticket et commencez a vendre vos billets des aujourd'hui.</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;">
<a href="{{ path('app_register') }}" style="padding:1rem 2rem;border:3px solid #111827;background:#111827;color:white;" class="font-black uppercase text-sm hover:bg-indigo-600 transition-all shadow-[4px_4px_0px_rgba(0,0,0,0.3)]">Creer un compte organisateur</a>
<a href="{{ path('app_tarifs') }}" style="padding:1rem 2rem;border:3px solid #111827;background:white;color:#111827;" class="font-black uppercase text-sm hover:bg-gray-100 transition-all">Voir les tarifs</a>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends 'base.html.twig' %}
{% block title %}Tarifs - E-Ticket{% endblock %}
{% block description %}Tarifs et commissions de la plateforme E-Ticket pour les organisateurs d'evenements{% endblock %}
{% block stylesheets %}
<script type="application/ld+json">
[
{
"@context": "https://schema.org",
"@type": "Product",
"name": "E-Ticket Gratuit",
"description": "Abonnement gratuit sur demande : 5 evenements simultanement, Billets illimites, QR Code securise, paiement Stripe",
"brand": {"@type": "Organization", "name": "E-Ticket"},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR",
"availability": "https://schema.org/OnlineOnly",
"url": "https://ticket.e-cosplay.fr/tarifs"
}
},
{
"@context": "https://schema.org",
"@type": "Product",
"name": "E-Ticket Basique",
"description": "Abonnement basique : evenements et billets illimites, commission reduite a 1.5%, page organisateur personnalisee",
"brand": {"@type": "Organization", "name": "E-Ticket"},
"offers": {
"@type": "Offer",
"price": "10",
"priceCurrency": "EUR",
"unitCode": "MON",
"availability": "https://schema.org/OnlineOnly",
"url": "https://ticket.e-cosplay.fr/tarifs"
}
},
{
"@context": "https://schema.org",
"@type": "Product",
"name": "E-Ticket Sur-mesure",
"description": "Abonnement sur-mesure : evenements et billets illimites, commission a partir de 0.5%, accompagnement dedie, tarif personnalise",
"brand": {"@type": "Organization", "name": "E-Ticket"},
"offers": {
"@type": "AggregateOffer",
"lowPrice": "0.5",
"priceCurrency": "EUR",
"availability": "https://schema.org/OnlineOnly",
"url": "https://ticket.e-cosplay.fr/tarifs"
}
}
]
</script>
{% endblock %}
{% block body %}
<div style="max-width:60rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Tarifs</h1>
<p class="font-bold text-gray-600 italic" style="margin-bottom:2rem;">Transparence totale sur nos commissions et abonnements.</p>
<h2 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;">Commission par transaction</h2>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-bottom:3rem;">
<div style="flex:1;min-width:250px;border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
<p style="font-size:10px;letter-spacing:0.1em;" class="font-black uppercase text-gray-500">Commission plateforme</p>
<p class="text-4xl font-black" style="margin:0.5rem 0;">3%</p>
<p class="text-sm font-bold text-gray-600">du montant de chaque billet vendu</p>
<p class="text-sm font-bold text-gray-600" style="margin-top:0.5rem;">Negociable selon votre profil, contactez-nous.</p>
<p style="margin-top:1rem;font-size:0.75rem;" class="text-gray-500 font-bold">+ frais Stripe (1.5% + 0.25&euro; par transaction, non negociables)</p>
</div>
<div style="flex:1;min-width:250px;border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
<p style="font-size:10px;letter-spacing:0.1em;" class="font-black uppercase text-gray-500">Billets gratuits</p>
<p class="text-4xl font-black" style="margin:0.5rem 0;">0&euro;</p>
<p class="text-sm font-bold text-gray-600">aucune commission sur les billets gratuits</p>
</div>
</div>
<h2 class="text-2xl font-black uppercase" style="margin-bottom:1.5rem;">Abonnements organisateur</h2>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-bottom:3rem;">
<div style="flex:1;min-width:250px;border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
<div style="background:#fabf04;border:2px solid #111827;display:inline-block;padding:0.25rem 0.75rem;margin-bottom:1rem;">
<p style="font-size:10px;letter-spacing:0.1em;" class="font-black uppercase">Gratuit</p>
</div>
<p class="text-4xl font-black" style="margin:0.5rem 0;">0&euro;<span class="text-sm font-bold text-gray-500">/mois</span></p>
<p class="text-sm font-bold text-gray-600" style="margin-bottom:0.5rem;">Sur demande, apres approbation par la plateforme</p>
<ul style="list-style:none;padding:0;margin-top:1rem;" class="text-sm font-bold">
<li style="padding:0.25rem 0;">&#10003; 5 evenements simultanement</li>
<li style="padding:0.25rem 0;">&#10003; Billets illimites</li>
<li style="padding:0.25rem 0;">&#10003; QR Code securise</li>
<li style="padding:0.25rem 0;">&#10003; Paiement Stripe</li>
<li style="padding:0.25rem 0;">&#10003; Email de confirmation</li>
</ul>
<a href="mailto:contact@e-cosplay.fr" style="display:inline-block;margin-top:1rem;padding:0.5rem 1rem;border:2px solid #111827;" class="bg-gray-900 text-white font-black uppercase text-xs hover:bg-indigo-600 transition-colors">Faire une demande</a>
</div>
<div style="flex:1;min-width:250px;border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
<div style="background:#4f46e5;color:white;border:2px solid #111827;display:inline-block;padding:0.25rem 0.75rem;margin-bottom:1rem;">
<p style="font-size:10px;letter-spacing:0.1em;" class="font-black uppercase">Basique</p>
</div>
<p class="text-4xl font-black" style="margin:0.5rem 0;">10&euro;<span class="text-sm font-bold text-gray-500">/mois</span></p>
<ul style="list-style:none;padding:0;margin-top:1rem;" class="text-sm font-bold">
<li style="padding:0.25rem 0;">&#10003; Evenements illimites</li>
<li style="padding:0.25rem 0;">&#10003; Billets illimites</li>
<li style="padding:0.25rem 0;">&#10003; Commission reduite a 1.5%</li>
<li style="padding:0.25rem 0;">&#10003; Page organisateur personnalisee</li>
</ul>
</div>
<div style="flex:1;min-width:250px;border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:#111827;color:white;">
<div style="background:#ec4899;color:white;border:2px solid white;display:inline-block;padding:0.25rem 0.75rem;margin-bottom:1rem;">
<p style="font-size:10px;letter-spacing:0.1em;" class="font-black uppercase">Sur-mesure</p>
</div>
<p class="text-4xl font-black" style="margin:0.5rem 0;">Sur devis</p>
<p class="text-sm font-bold" style="opacity:0.8;">Tarif personnalise selon vos besoins</p>
<ul style="list-style:none;padding:0;margin-top:1rem;" class="text-sm font-bold">
<li style="padding:0.25rem 0;">&#10003; Evenements illimites</li>
<li style="padding:0.25rem 0;">&#10003; Billets illimites</li>
<li style="padding:0.25rem 0;">&#10003; Commission a partir de 0.5%</li>
<li style="padding:0.25rem 0;">&#10003; Page organisateur personnalisee</li>
<li style="padding:0.25rem 0;">&#10003; Reservation de tables</li>
<li style="padding:0.25rem 0;">&#10003; Gestion de brocantes</li>
<li style="padding:0.25rem 0;">&#10003; Billets personnalisables</li>
<li style="padding:0.25rem 0;">&#10003; Accompagnement dedie</li>
</ul>
<a href="mailto:contact@e-cosplay.fr" style="display:inline-block;margin-top:1rem;padding:0.5rem 1rem;border:2px solid white;" class="bg-white text-gray-900 font-black uppercase text-xs hover:bg-yellow-400 transition-colors">Nous contacter</a>
</div>
</div>
<div style="border:4px solid #111827;padding:1.5rem;background:#f9fafb;box-shadow:6px 6px 0 rgba(0,0,0,1);">
<h3 class="text-lg font-black uppercase" style="margin-bottom:0.5rem;">Inclus dans toutes les formules</h3>
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
<li>Paiement securise via Stripe</li>
<li>Billets electroniques avec QR Code</li>
<li>Emails signes S/MIME</li>
<li>Scan des billets a l'entree</li>
<li>Conformite RGPD</li>
<li>HTTPS et protection Cloudflare</li>
</ul>
</div>
<p class="text-sm opacity-70 italic" style="margin-top:2rem;">Les tarifs sont exprimes en euros HT. Contact : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></p>
</div>
{% endblock %}

View File

@@ -1,7 +1,180 @@
{% extends 'base.html.twig' %}
{% block title %}CGU - E-Ticket{% endblock %}
{% block description %}Conditions Generales d'Utilisation de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Conditions Generales d'Utilisation</h1>
<div style="display:flex;gap:0.5rem;margin-bottom:2rem;">
<button id="btn-acheteur" data-tab="tab-acheteur" style="padding:0.75rem 1.5rem;font-weight:900;text-transform:uppercase;font-size:0.75rem;letter-spacing:0.1em;border:2px solid #111827;background:#111827;color:white;cursor:pointer;">Acheteur</button>
<button id="btn-organisateur" data-tab="tab-organisateur" style="padding:0.75rem 1.5rem;font-weight:900;text-transform:uppercase;font-size:0.75rem;letter-spacing:0.1em;border:2px solid #111827;background:white;color:#111827;cursor:pointer;">Organisateur</button>
</div>
<div id="tab-acheteur" style="display:block;">
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Objet</h2>
<p>Les presentes Conditions Generales d'Utilisation (CGU) regissent l'utilisation de la plateforme E-Ticket (ticket.e-cosplay.fr) par les acheteurs de billets. En achetant un billet, vous acceptez sans reserve les presentes CGU.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Inscription</h2>
<p>L'achat de billets necessite la creation d'un compte utilisateur. Vous vous engagez a fournir des informations exactes et a jour. Vous etes responsable de la confidentialite de vos identifiants de connexion.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Achat de billets</h2>
<p>L'achat d'un billet constitue un contrat directement entre <strong>l'acheteur et l'organisateur de l'evenement</strong>. La Plateforme E-Ticket agit uniquement en tant qu'intermediaire technique facilitant la mise en relation et le paiement.</p>
<p style="margin-top:0.5rem;">Le prix des billets est fixe par l'organisateur. Une commission de service peut etre appliquee par la Plateforme, dont le montant est indique avant la validation du paiement.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Paiement</h2>
<p>Les paiements sont securises par Stripe. La Plateforme ne stocke aucune donnee bancaire. Le paiement est debite immediatement a la validation de la commande.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Billets et QR Code</h2>
<p>Apres validation du paiement, un billet electronique comportant un QR Code unique est envoye par email. Ce billet est personnel et ne peut etre revendu. Toute duplication ou falsification est interdite et pourra donner lieu a des poursuites.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Annulation et remboursement</h2>
<p><strong>L'organisateur est seul responsable</strong> de la politique d'annulation et de remboursement de ses evenements. En cas d'annulation d'un evenement :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>L'acheteur doit contacter directement l'organisateur pour toute demande de remboursement</li>
<li>La Plateforme E-Ticket n'est pas responsable des remboursements</li>
<li>La commission de service de la Plateforme n'est pas remboursable</li>
</ul>
<p style="margin-top:0.5rem;">Conformement a l'article L221-28 du Code de la consommation, le droit de retractation ne s'applique pas aux prestations de services de loisirs devant etre fournis a une date determinee.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Responsabilite de la Plateforme</h2>
<p>La Plateforme E-Ticket decline toute responsabilite concernant :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>L'organisation, le deroulement ou l'annulation des evenements</li>
<li>La qualite ou la securite des evenements</li>
<li>Les litiges entre acheteurs et organisateurs</li>
<li>Les dommages directs ou indirects lies a la participation a un evenement</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. Donnees personnelles</h2>
<p>Vos donnees sont traitees conformement a notre <a href="{{ path('app_rgpd') }}" class="text-indigo-600 hover:underline">Politique de confidentialite</a>. Vos donnees (nom, email) sont transmises a l'organisateur de l'evenement pour la gestion de votre billet.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">9. Comportement</h2>
<p>L'acheteur s'engage a :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Ne pas utiliser la Plateforme a des fins frauduleuses</li>
<li>Ne pas revendre les billets achetes</li>
<li>Ne pas perturber le fonctionnement de la Plateforme</li>
<li>Respecter le reglement interieur de l'evenement</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">10. Droit applicable</h2>
<p>Les presentes CGU sont soumises au droit francais. Tout litige sera soumis aux tribunaux competents de Laon.</p>
</section>
</div>
</div>
<div id="tab-organisateur" style="display:none;">
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Objet</h2>
<p>Les presentes CGU regissent l'utilisation de la plateforme E-Ticket par les organisateurs d'evenements. En publiant un evenement, vous acceptez sans reserve les presentes CGU.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Inscription organisateur</h2>
<p>Pour publier un evenement, l'organisateur doit creer un compte et fournir des informations exactes sur son identite (association, entreprise ou particulier). La Plateforme se reserve le droit de verifier ces informations et de refuser ou suspendre un compte en cas d'informations inexactes.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Publication d'evenements</h2>
<p>L'organisateur est seul responsable du contenu publie sur la Plateforme (descriptions, images, tarifs, dates, conditions). Il garantit que :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Les informations publiees sont exactes et a jour</li>
<li>Il dispose de tous les droits necessaires sur les contenus publies (images, textes, logos)</li>
<li>L'evenement respecte la legislation en vigueur</li>
<li>Il dispose des autorisations, assurances et licences necessaires a l'organisation de l'evenement</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Tarification et paiement</h2>
<p>L'organisateur fixe librement le prix de ses billets. La Plateforme preleve une commission de service sur chaque vente, dont le taux est communique a l'organisateur avant la mise en vente.</p>
<p style="margin-top:0.5rem;">Les fonds collectes sont reverses a l'organisateur via Stripe Connect selon les modalites suivantes :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Reversement apres l'evenement ou selon le calendrier defini avec l'organisateur</li>
<li>Deduction de la commission de la Plateforme et des frais Stripe</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Responsabilite de l'organisateur</h2>
<p><strong>L'organisateur est entierement responsable de :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>L'organisation, le deroulement et la securite de son evenement</li>
<li>Le respect de la reglementation applicable (ERP, securite, assurances, SACEM, etc.)</li>
<li>La gestion des reclamations et remboursements envers les acheteurs</li>
<li>La conformite des informations publiees</li>
<li>La declaration fiscale des revenus generes par la vente de billets</li>
</ul>
<p style="margin-top:0.5rem;">L'organisateur s'engage a <strong>indemniser la Plateforme</strong> de tout prejudice, reclamation ou action resultant du non-respect de ses obligations.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Annulation d'evenement</h2>
<p>En cas d'annulation d'un evenement, l'organisateur s'engage a :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Informer immediatement la Plateforme et les acheteurs</li>
<li>Proceder au remboursement integral des acheteurs dans un delai de 30 jours</li>
<li>La commission de la Plateforme reste due sauf accord contraire</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Donnees des acheteurs</h2>
<p>L'organisateur recoit les donnees des acheteurs (nom, email) strictement pour la gestion de son evenement. Il s'engage a :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Ne pas utiliser ces donnees a d'autres fins (prospection, revente, etc.)</li>
<li>Respecter le RGPD dans le traitement de ces donnees</li>
<li>Supprimer les donnees dans un delai raisonnable apres l'evenement</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. Moderation</h2>
<p>La Plateforme se reserve le droit de :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Refuser, suspendre ou supprimer un evenement sans preavis en cas de non-respect des CGU</li>
<li>Suspendre ou supprimer le compte d'un organisateur en cas de fraude, abus ou plaintes repetees</li>
<li>Retenir les fonds en cas de litige ou de suspicion de fraude</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">9. Responsabilite de la Plateforme</h2>
<p>La Plateforme agit en tant qu'intermediaire technique et decline toute responsabilite concernant les evenements publies par les organisateurs. La Plateforme ne garantit pas le succes commercial d'un evenement.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">10. Droit applicable</h2>
<p>Les presentes CGU sont soumises au droit francais. Tout litige sera soumis aux tribunaux competents de Laon.</p>
</section>
</div>
</div>
<p class="text-sm opacity-70 italic" style="margin-top:2rem;">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
{% endblock %}

View File

@@ -1,7 +1,137 @@
{% extends 'base.html.twig' %}
{% block title %}CGV - E-Ticket{% endblock %}
{% block description %}Conditions Generales de Vente de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Conditions Generales de Vente</h1>
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Objet</h2>
<p>Les presentes Conditions Generales de Vente (CGV) regissent les transactions effectuees sur la plateforme E-Ticket (ticket.e-cosplay.fr), editee par l'association E-Cosplay (SIREN 943121517). Elles s'appliquent a toute vente de billets realisee via la Plateforme.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Role de la Plateforme</h2>
<p>La Plateforme E-Ticket agit en qualite d'<strong>intermediaire technique</strong> entre les organisateurs d'evenements et les acheteurs de billets. La Plateforme :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Met a disposition un outil de creation et de vente de billets</li>
<li>Assure le traitement securise des paiements via Stripe</li>
<li>Genere et envoie les billets electroniques (QR Code)</li>
<li><strong>N'est pas partie au contrat de vente</strong> conclu entre l'organisateur et l'acheteur</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Prix et commission</h2>
<p>Les prix des billets sont fixes librement par chaque organisateur et affiches en euros (EUR) toutes taxes comprises.</p>
<p style="margin-top:0.5rem;">Une <strong>commission de service</strong> est appliquee par la Plateforme sur chaque transaction. Le montant de cette commission est clairement indique a l'acheteur avant la validation du paiement et a l'organisateur lors de la creation de l'evenement.</p>
<p style="margin-top:0.5rem;">La commission comprend :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Les frais de fonctionnement de la Plateforme</li>
<li>Les frais de traitement du paiement (Stripe)</li>
<li>La generation et l'envoi du billet electronique</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Processus d'achat</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li>L'acheteur selectionne un evenement et le nombre de billets souhaites</li>
<li>Le recapitulatif de la commande (prix, commission, total) est affiche avant validation</li>
<li>Le paiement est effectue en ligne via Stripe (carte bancaire)</li>
<li>Un email de confirmation contenant le(s) billet(s) avec QR Code est envoye apres validation du paiement</li>
<li>La commande est consideree comme definitive des la confirmation du paiement</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Billets electroniques</h2>
<p>Chaque billet est unique et comporte un QR Code securise. Le billet est :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Personnel et nominatif</li>
<li>Non cessible et non revendable</li>
<li>Valable uniquement pour l'evenement et la date indiques</li>
<li>Signe electroniquement (S/MIME) pour garantir son authenticite</li>
</ul>
<p style="margin-top:0.5rem;">Toute falsification, duplication ou revente de billets est interdite et passible de poursuites judiciaires.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Annulation, remboursement et droit de retractation</h2>
<p>Conformement a l'article L221-28 12&deg; du Code de la consommation, <strong>le droit de retractation ne s'applique pas</strong> aux prestations de services de loisirs devant etre fournies a une date determinee.</p>
<p style="margin-top:0.75rem;"><strong>En cas d'annulation par l'organisateur :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.25rem;">
<li>L'organisateur est seul responsable du remboursement des acheteurs</li>
<li>Le remboursement doit intervenir dans un delai de 30 jours suivant l'annulation</li>
<li>La commission de la Plateforme n'est pas remboursable, sauf accord contraire</li>
</ul>
<p style="margin-top:0.75rem;"><strong>En cas de modification par l'organisateur</strong> (changement de date, lieu, programme) :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.25rem;">
<li>L'organisateur doit informer les acheteurs dans les meilleurs delais</li>
<li>L'acheteur peut demander un remboursement a l'organisateur si la modification est substantielle</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Reversement aux organisateurs</h2>
<p>Les fonds collectes sont reverses aux organisateurs via Stripe Connect :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Apres la tenue de l'evenement, ou selon le calendrier convenu</li>
<li>Apres deduction de la commission de la Plateforme et des frais Stripe</li>
<li>La Plateforme se reserve le droit de retenir les fonds en cas de litige, fraude ou reclamation</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. CGV specifiques des organisateurs</h2>
<p>Chaque organisateur peut definir ses propres conditions de vente specifiques a ses evenements (politique de remboursement, conditions d'acces, reglement interieur, etc.).</p>
<p style="margin-top:0.5rem;"><strong>Les conditions specifiques de l'organisateur ne doivent en aucun cas etre en conflit avec les presentes CGV de la Plateforme.</strong> En cas de contradiction, les presentes CGV prevalent.</p>
<p style="margin-top:0.5rem;">Les conditions specifiques de l'organisateur sont affichees sur la page de l'evenement et doivent etre acceptees par l'acheteur avant l'achat. L'organisateur est seul responsable du contenu et de la legalite de ses conditions specifiques.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">9. Responsabilite</h2>
<p>La Plateforme E-Ticket, en tant qu'intermediaire technique :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>N'est pas responsable</strong> de l'organisation, du deroulement ou de l'annulation des evenements</li>
<li><strong>N'est pas responsable</strong> des litiges entre organisateurs et acheteurs</li>
<li><strong>N'est pas responsable</strong> des dommages directs ou indirects lies a un evenement</li>
<li>S'engage a assurer la disponibilite et la securite de la Plateforme dans la mesure du possible</li>
</ul>
<p style="margin-top:0.5rem;">L'organisateur est seul responsable de ses evenements et de ses obligations envers les acheteurs.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">10. Propriete intellectuelle</h2>
<p>L'ensemble des elements de la Plateforme (code, design, logo, textes) est la propriete exclusive de l'association E-Cosplay. Les contenus publies par les organisateurs restent leur propriete.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">11. Donnees personnelles</h2>
<p>Les donnees personnelles collectees dans le cadre des ventes sont traitees conformement a notre <a href="{{ path('app_rgpd') }}" class="text-indigo-600 hover:underline">Politique de confidentialite</a> et au RGPD.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">12. Mediation</h2>
<p>En cas de litige, l'acheteur peut recourir gratuitement au service de mediation suivant :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Plateforme de reglement en ligne des litiges de la Commission Europeenne : ec.europa.eu/consumers/odr</li>
</ul>
<p style="margin-top:0.5rem;">Avant toute mediation, l'acheteur est invite a contacter la Plateforme a l'adresse <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a>.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">13. Droit applicable</h2>
<p>Les presentes CGV sont soumises au droit francais. Tout litige sera soumis aux tribunaux competents de Laon.</p>
</section>
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,86 @@
{% extends 'base.html.twig' %}
{% block title %}Cookies - E-Ticket{% endblock %}
{% block title %}Politique de Cookies - E-Ticket{% endblock %}
{% block description %}Politique de cookies de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Politique de Cookies</h1>
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Qu'est-ce qu'un cookie ?</h2>
<p>Un cookie est un petit fichier texte depose sur votre terminal (ordinateur, tablette, smartphone) lors de la visite d'un site web. Il permet au site de memoriser des informations relatives a votre navigation (preferences, session, etc.).</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Cookies utilises sur la Plateforme</h2>
<p>La plateforme E-Ticket utilise exclusivement des <strong>cookies strictement necessaires</strong> au fonctionnement du site :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Cookie de session</strong> : permet de maintenir votre connexion et votre navigation sur la Plateforme. Il est supprime a la fermeture du navigateur.</li>
<li><strong>Cookie de securite (CSRF)</strong> : protege contre les attaques de type Cross-Site Request Forgery lors de la soumission de formulaires.</li>
<li><strong>Cookie de preference</strong> : memorise vos choix (langue, theme) pour ameliorer votre experience utilisateur.</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Cookies tiers</h2>
<p>La Plateforme peut integrer des services tiers qui deposent leurs propres cookies :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Stripe</strong> : pour le traitement securise des paiements. Ces cookies sont necessaires au fonctionnement du module de paiement.</li>
<li><strong>Cloudflare</strong> : pour la securite et la performance du site (protection DDoS, CDN). Ces cookies sont strictement techniques.</li>
</ul>
<p style="margin-top:0.5rem;">La Plateforme <strong>n'utilise aucun cookie publicitaire, de tracking ou d'analyse comportementale</strong> (pas de Google Analytics, Facebook Pixel, etc.).</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Duree de conservation</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Cookies de session</strong> : supprimes a la fermeture du navigateur</li>
<li><strong>Cookies de securite (Cloudflare)</strong> : duree maximale de 24 heures</li>
<li><strong>Cookies Stripe</strong> : selon la politique de Stripe, generalement le temps de la transaction</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Gestion des cookies</h2>
<p>Etant donne que la Plateforme utilise uniquement des cookies strictement necessaires, aucun consentement prealable n'est requis conformement a l'article 82 de la loi Informatique et Libertes et aux recommandations de la CNIL.</p>
<p style="margin-top:0.5rem;">Vous pouvez toutefois configurer votre navigateur pour refuser les cookies. Veuillez noter que la desactivation des cookies necessaires peut empecher le bon fonctionnement de la Plateforme (connexion, paiement, etc.).</p>
<p style="margin-top:0.5rem;">Pour configurer les cookies dans votre navigateur :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Chrome : Parametres &gt; Confidentialite et securite &gt; Cookies</li>
<li>Firefox : Parametres &gt; Vie privee et securite &gt; Cookies</li>
<li>Safari : Preferences &gt; Confidentialite &gt; Cookies</li>
<li>Edge : Parametres &gt; Cookies et autorisations de site</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Base legale</h2>
<p>Le depot de cookies strictement necessaires repose sur l'<strong>interet legitime</strong> de l'editeur a assurer le fonctionnement et la securite de la Plateforme, conformement a :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Article 82 de la loi n&deg;78-17 du 6 janvier 1978 (Informatique et Libertes)</li>
<li>Directive 2002/58/CE (directive ePrivacy)</li>
<li>Recommandations de la CNIL sur les cookies et traceurs</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Delegue a la Protection des Donnees</h2>
<p>Pour toute question relative aux cookies ou a vos donnees personnelles :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>DPO : <strong>DPO-167945</strong></li>
<li>Email : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. Droit applicable</h2>
<p>Tout litige en relation avec l'utilisation des cookies sur la Plateforme est soumis au droit francais. Il est fait attribution exclusive de juridiction aux tribunaux competents de Laon.</p>
</section>
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,80 @@
{% extends 'base.html.twig' %}
{% block title %}Hebergement - E-Ticket{% endblock %}
{% block description %}Informations sur l'hebergement de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Hebergement</h1>
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Hebergeur principal</h2>
<p>Le site <strong>ticket.e-cosplay.fr</strong> est heberge par :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Google Cloud Platform (GCP)</strong></li>
<li>Google Ireland Limited</li>
<li>Gordon House, Barrow Street, Dublin 4, Irlande</li>
<li>Region : europe-west1 (Belgique)</li>
<li>Site : cloud.google.com</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. CDN et securite</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Cloudflare, Inc.</strong></li>
<li>101 Townsend St, San Francisco, CA 94107, Etats-Unis</li>
<li>Services : CDN, protection DDoS, WAF, gestion DNS, certificats TLS</li>
<li>Site : cloudflare.com</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Stockage des fichiers</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Stockage objet compatible S3</strong></li>
<li>Heberge sur infrastructure ESY-WEB (s3.esy-web.dev)</li>
<li>Localisation : Europe</li>
<li>Utilisation : stockage des images, documents et fichiers uploades</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Service d'envoi d'emails</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Amazon Simple Email Service (SES)</strong></li>
<li>Amazon Web Services EMEA SARL</li>
<li>38 Avenue John F. Kennedy, L-1855 Luxembourg</li>
<li>Region : eu-west-3 (Paris)</li>
<li>Utilisation : envoi des emails transactionnels (billets, confirmations, notifications)</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Service de paiement</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Stripe Payments Europe, Ltd.</strong></li>
<li>1 Grand Canal Street Lower, Grand Canal Dock, Dublin 2, Irlande</li>
<li>Utilisation : traitement des paiements en ligne, gestion des remboursements</li>
<li>Certification : PCI DSS Level 1</li>
<li>Site : stripe.com</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Localisation des donnees</h2>
<p>L'ensemble des donnees de la Plateforme (base de donnees, fichiers, sauvegardes) est heberge dans l'<strong>Union Europeenne</strong>.</p>
<p style="margin-top:0.5rem;">Les sous-traitants americains (Cloudflare, Stripe) operent sous le cadre du <strong>Data Privacy Framework (DPF)</strong> et/ou des <strong>Clauses Contractuelles Types (CCT)</strong> pour les transferts de donnees hors UE.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Contact</h2>
<p>Pour toute question relative a l'hebergement : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></p>
</section>
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,95 @@
{% extends 'base.html.twig' %}
{% block title %}Mentions legales - E-Ticket{% endblock %}
{% block description %}Mentions legales de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Mentions Legales</h1>
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Editeur du site</h2>
<p>Le site <strong>ticket.e-cosplay.fr</strong> (ci-apres "la Plateforme") est edite par :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Association E-Cosplay</strong></li>
<li>RNA : W022006988</li>
<li>SIREN : 943121517</li>
<li>Siege social : 42 rue de Saint-Quentin, 02800 Beautor, France</li>
<li>Email : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></li>
<li>Telephone : 06 79 34 88 02</li>
<li>Directeur de la publication : Serreau Jovann</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Hebergement</h2>
<p>Le site est heberge par :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Google Cloud Platform</strong></li>
<li>Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irlande</li>
</ul>
<p style="margin-top:0.5rem;">Le nom de domaine est gere via <strong>Cloudflare, Inc.</strong>, 101 Townsend St, San Francisco, CA 94107, Etats-Unis.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Nature de la Plateforme</h2>
<p>La Plateforme E-Ticket est un <strong>intermediaire technique</strong> qui met a disposition des organisateurs d'evenements (associations, collectifs, particuliers) un outil de creation et de vente de billets en ligne.</p>
<p style="margin-top:0.5rem;">La Plateforme <strong>n'organise aucun evenement</strong> et <strong>n'est pas partie prenante</strong> aux transactions effectuees entre les organisateurs et les acheteurs de billets.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Responsabilite</h2>
<p><strong>L'association E-Cosplay, en tant qu'editeur de la Plateforme, decline toute responsabilite concernant :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>L'organisation, le deroulement, l'annulation ou la modification des evenements publies sur la Plateforme</li>
<li>La qualite, la conformite ou la securite des evenements proposes par les organisateurs</li>
<li>Les litiges commerciaux entre les organisateurs et les acheteurs (remboursements, reclamations, etc.)</li>
<li>Les informations publiees par les organisateurs sur leurs evenements (descriptions, dates, tarifs, etc.)</li>
<li>Les dommages directs ou indirects resultant de l'utilisation de la Plateforme ou de la participation a un evenement</li>
</ul>
<p style="margin-top:0.5rem;"><strong>Chaque organisateur est seul responsable</strong> de son evenement, de la vente de ses billets, du respect de la reglementation applicable (securite, assurance, droits d'auteur, etc.) et de ses obligations envers les acheteurs, notamment en matiere de remboursement.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Paiement</h2>
<p>Les paiements en ligne sont securises par <strong>Stripe</strong> (Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Irlande). La Plateforme ne stocke aucune donnee bancaire.</p>
<p style="margin-top:0.5rem;">Les fonds collectes sont reverses directement aux organisateurs via Stripe Connect. L'association E-Cosplay peut percevoir une commission sur chaque transaction, dont le montant est indique dans les CGV.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Propriete intellectuelle</h2>
<p>L'ensemble du contenu de la Plateforme (textes, graphismes, logos, icones, code source) est la propriete exclusive de l'association E-Cosplay, sauf mention contraire. Toute reproduction, meme partielle, est interdite sans autorisation ecrite prealable.</p>
<p style="margin-top:0.5rem;">Les contenus publies par les organisateurs (textes, images, logos) restent leur propriete. En publiant sur la Plateforme, ils accordent a E-Cosplay une licence non exclusive d'utilisation a des fins d'affichage et de promotion sur la Plateforme.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Donnees personnelles</h2>
<p>Conformement au Reglement General sur la Protection des Donnees (RGPD), l'association E-Cosplay s'engage a proteger la confidentialite des donnees personnelles collectees. Pour toute information ou exercice de vos droits Informatique et Libertes sur les traitements de donnees personnelles, vous pouvez contacter notre Delegue a la Protection des Donnees (DPO).</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Identifiant DPO : <strong>DPO-167945</strong></li>
<li>Contact DPO : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></li>
</ul>
<p style="margin-top:0.5rem;">Pour plus d'informations, consultez notre <a href="{{ path('app_rgpd') }}" class="text-indigo-600 hover:underline">Politique de confidentialite</a>.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. Cookies</h2>
<p>La Plateforme utilise des cookies strictement necessaires a son fonctionnement. Pour plus de details, consultez notre <a href="{{ path('app_cookies') }}" class="text-indigo-600 hover:underline">Politique de cookies</a>.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">9. Droit applicable et litiges</h2>
<p>Les presentes mentions legales sont regies par le droit francais. En cas de litige, les tribunaux competents de Laon (Aisne) seront seuls competents.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">10. Contact</h2>
<p>Pour toute question relative aux presentes mentions legales, vous pouvez nous contacter a l'adresse : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a>.</p>
</section>
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,157 @@
{% extends 'base.html.twig' %}
{% block title %}RGPD - E-Ticket{% endblock %}
{% block title %}Politique RGPD - E-Ticket{% endblock %}
{% block description %}Politique de confidentialite et protection des donnees personnelles de la plateforme E-Ticket{% endblock %}
{% block body %}
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:2rem;">Politique de Confidentialite</h1>
<div style="display:flex;flex-direction:column;gap:2rem;">
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Responsable du traitement</h2>
<p>Le responsable du traitement des donnees personnelles est :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Association E-Cosplay</strong></li>
<li>SIREN : 943121517 / RNA : W022006988</li>
<li>42 rue de Saint-Quentin, 02800 Beautor, France</li>
<li>Email : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Delegue a la Protection des Donnees (DPO)</h2>
<p>Conformement au RGPD, un Delegue a la Protection des Donnees a ete designe :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Identifiant DPO : <strong>DPO-167945</strong></li>
<li>Contact : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Donnees collectees</h2>
<p>La Plateforme collecte les donnees suivantes :</p>
<p style="margin-top:0.75rem;"><strong>Lors de la creation de compte :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.25rem;">
<li>Nom et prenom</li>
<li>Adresse email</li>
<li>Mot de passe (stocke sous forme hashee)</li>
</ul>
<p style="margin-top:0.75rem;"><strong>Lors de l'achat de billets :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.25rem;">
<li>Nom et prenom de l'acheteur</li>
<li>Adresse email</li>
<li>Donnees de paiement (traitees exclusivement par Stripe, non stockees sur nos serveurs)</li>
</ul>
<p style="margin-top:0.75rem;"><strong>Donnees techniques :</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.25rem;">
<li>Adresse IP (anonymisee via Cloudflare)</li>
<li>Cookies strictement necessaires (voir <a href="{{ path('app_cookies') }}" class="text-indigo-600 hover:underline">Politique de cookies</a>)</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Finalites du traitement</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li>Gestion des comptes utilisateurs</li>
<li>Traitement des commandes et emission des billets</li>
<li>Envoi des billets et confirmations par email</li>
<li>Communication liee aux evenements achetes (modifications, annulations)</li>
<li>Securite de la Plateforme et prevention des fraudes</li>
<li>Respect des obligations legales et reglementaires</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Bases legales</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Execution du contrat</strong> : traitement des commandes, emission des billets, gestion du compte</li>
<li><strong>Obligation legale</strong> : conservation des donnees de facturation</li>
<li><strong>Interet legitime</strong> : securite de la Plateforme, prevention des fraudes</li>
<li><strong>Consentement</strong> : envoi de communications commerciales (newsletter)</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">6. Destinataires des donnees</h2>
<p>Vos donnees personnelles sont accessibles par :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>L'association E-Cosplay</strong> : administration de la Plateforme</li>
<li><strong>Les organisateurs d'evenements</strong> : uniquement les donnees necessaires a la gestion de leurs evenements (nom, email de l'acheteur)</li>
<li><strong>Stripe</strong> : traitement des paiements (Stripe Payments Europe, Ltd., Dublin, Irlande)</li>
<li><strong>Amazon Web Services (SES)</strong> : envoi des emails transactionnels (region eu-west-3, Irlande)</li>
<li><strong>Google Cloud Platform</strong> : hebergement des donnees (region Europe)</li>
<li><strong>Cloudflare</strong> : securite et CDN</li>
</ul>
<p style="margin-top:0.5rem;">Aucune donnee n'est vendue ou cedee a des tiers a des fins commerciales ou publicitaires.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">7. Transferts hors UE</h2>
<p>Certains sous-traitants (Stripe, Cloudflare) peuvent transferer des donnees en dehors de l'Union Europeenne. Ces transferts sont encadres par :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Les Clauses Contractuelles Types (CCT) de la Commission Europeenne</li>
<li>Le Data Privacy Framework (DPF) UE-US pour les entreprises certifiees</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">8. Duree de conservation</h2>
<ul style="list-style:disc;padding-left:1.5rem;">
<li><strong>Donnees de compte</strong> : conservees pendant la duree d'existence du compte, puis 3 ans apres la derniere activite</li>
<li><strong>Donnees de transaction</strong> : 10 ans (obligation legale comptable)</li>
<li><strong>Donnees de connexion (logs)</strong> : 12 mois</li>
<li><strong>Donnees de prospection</strong> : 3 ans apres le dernier contact</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">9. Securite des donnees</h2>
<p>L'association E-Cosplay met en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la securite des donnees :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>Chiffrement des communications (TLS/HTTPS)</li>
<li>Hashage des mots de passe (bcrypt)</li>
<li>Signature S/MIME des emails</li>
<li>Protection DDoS et WAF via Cloudflare</li>
<li>Acces restreint aux donnees (principe du moindre privilege)</li>
<li>Sauvegardes regulieres et chiffrees</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">10. Vos droits</h2>
<p>Conformement au RGPD, vous disposez des droits suivants :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li><strong>Droit d'acces</strong> : obtenir une copie de vos donnees personnelles</li>
<li><strong>Droit de rectification</strong> : corriger des donnees inexactes ou incompletes</li>
<li><strong>Droit a l'effacement</strong> : demander la suppression de vos donnees (sous reserve des obligations legales)</li>
<li><strong>Droit a la limitation</strong> : limiter le traitement dans certains cas</li>
<li><strong>Droit a la portabilite</strong> : recevoir vos donnees dans un format structure et lisible</li>
<li><strong>Droit d'opposition</strong> : vous opposer au traitement de vos donnees pour des motifs legitimes</li>
<li><strong>Droit de retrait du consentement</strong> : retirer votre consentement a tout moment (newsletter, etc.)</li>
</ul>
<p style="margin-top:0.5rem;">Pour exercer vos droits, contactez le DPO a l'adresse <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a> en precisant votre identite. Une reponse vous sera apportee dans un delai maximum de 30 jours.</p>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">11. Reclamation</h2>
<p>Si vous estimez que le traitement de vos donnees personnelles constitue une violation du RGPD, vous avez le droit d'introduire une reclamation aupres de la Commission Nationale de l'Informatique et des Libertes (CNIL) :</p>
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem;">
<li>CNIL - 3 Place de Fontenoy, TSA 80715, 75334 Paris Cedex 07</li>
<li>Site : www.cnil.fr</li>
</ul>
</section>
<section>
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">12. Droit applicable</h2>
<p>Tout litige en relation avec le traitement des donnees personnelles est soumis au droit francais. Il est fait attribution exclusive de juridiction aux tribunaux competents de Laon.</p>
</section>
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
</div>
</div>
{% endblock %}

View File

@@ -3,6 +3,7 @@
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Mailer\MailerInterface;
class CspReportControllerTest extends WebTestCase
{
@@ -53,4 +54,28 @@ class CspReportControllerTest extends WebTestCase
self::assertResponseStatusCodeSame(204);
}
public function testRealViolationHandlesMailerFailure(): void
{
$client = static::createClient();
$mailer = $this->createMock(MailerInterface::class);
$mailer->method('send')->willThrowException(new \RuntimeException('SMTP down'));
static::getContainer()->set(MailerInterface::class, $mailer);
$payload = json_encode([
'csp-report' => [
'source-file' => 'https://evil.com/script.js',
'blocked-uri' => 'https://evil.com',
'document-uri' => 'https://e-cosplay.fr/page',
'violated-directive' => 'script-src',
],
]);
$client->request('POST', '/my-csp-report', [], [], [
'CONTENT_TYPE' => 'application/json',
], $payload);
self::assertResponseStatusCodeSame(204);
}
}

View File

@@ -2,26 +2,48 @@
namespace App\Tests\Controller;
use App\Entity\EmailTracking;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class EmailTrackingControllerTest extends WebTestCase
{
public function testTrackReturnsImageResponse(): void
private function ensureLogoExists(): void
{
$client = static::createClient();
$projectDir = static::getContainer()->getParameter('kernel.project_dir');
$logoPath = $projectDir.'/public/logo.jpg';
if (!file_exists($logoPath)) {
file_put_contents($logoPath, 'fake-image');
}
}
public function testTrackReturnsImageResponse(): void
{
$client = static::createClient();
$this->ensureLogoExists();
$client->request('GET', '/track/nonexistent-id/logo.jpg');
self::assertResponseIsSuccessful();
if ('fake-image' === file_get_contents($logoPath)) {
unlink($logoPath);
}
public function testTrackMarksAsOpened(): void
{
$client = static::createClient();
$this->ensureLogoExists();
$em = static::getContainer()->get(EntityManagerInterface::class);
$tracking = new EmailTracking('test-track-'.uniqid(), 'user@example.com', 'Test Subject');
$em->persist($tracking);
$em->flush();
$client->request('GET', '/track/'.$tracking->getMessageId().'/logo.jpg');
self::assertResponseIsSuccessful();
$em->refresh($tracking);
self::assertSame('opened', $tracking->getState());
self::assertNotNull($tracking->getOpenedAt());
}
}

View File

@@ -13,4 +13,12 @@ class HomeControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testTarifsReturnsSuccess(): void
{
$client = static::createClient();
$client->request('GET', '/tarifs');
self::assertResponseIsSuccessful();
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class LegalControllerTest extends WebTestCase
{
public function testMentionsLegales(): void
{
$client = static::createClient();
$client->request('GET', '/mentions-legales');
self::assertResponseIsSuccessful();
}
public function testCgu(): void
{
$client = static::createClient();
$client->request('GET', '/cgu');
self::assertResponseIsSuccessful();
}
public function testCgv(): void
{
$client = static::createClient();
$client->request('GET', '/cgv');
self::assertResponseIsSuccessful();
}
public function testHosting(): void
{
$client = static::createClient();
$client->request('GET', '/hebergement');
self::assertResponseIsSuccessful();
}
public function testCookies(): void
{
$client = static::createClient();
$client->request('GET', '/cookies');
self::assertResponseIsSuccessful();
}
public function testRgpd(): void
{
$client = static::createClient();
$client->request('GET', '/rgpd');
self::assertResponseIsSuccessful();
}
}

View File

@@ -2,10 +2,31 @@
namespace App\Tests\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class RegistrationControllerTest extends WebTestCase
{
public function testRegistrationRedirectsWhenAuthenticated(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$user = new User();
$user->setEmail('test-reg-auth-'.uniqid().'@example.com');
$user->setFirstName('Test');
$user->setLastName('User');
$user->setPassword('$2y$13$hashed');
$em->persist($user);
$em->flush();
$client->loginUser($user);
$client->request('GET', '/inscription');
self::assertResponseRedirects();
}
public function testRegistrationPageReturnsSuccess(): void
{
$client = static::createClient();

View File

@@ -2,6 +2,8 @@
namespace App\Tests\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SecurityControllerTest extends WebTestCase
@@ -14,6 +16,47 @@ class SecurityControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testLoginRedirectsWhenAuthenticated(): void
{
$client = static::createClient();
$user = $this->createUser();
$client->loginUser($user);
$client->request('GET', '/connexion');
self::assertResponseRedirects();
}
public function testChangePasswordRedirectsWhenNotAuthenticated(): void
{
$client = static::createClient();
$client->request('GET', '/mot-de-passe');
self::assertResponseRedirects();
}
public function testChangePasswordReturnsSuccessWhenAuthenticated(): void
{
$client = static::createClient();
$user = $this->createUser();
$client->loginUser($user);
$client->request('GET', '/mot-de-passe');
self::assertResponseIsSuccessful();
}
public function testWellKnownChangePasswordWhenAuthenticated(): void
{
$client = static::createClient();
$user = $this->createUser();
$client->loginUser($user);
$client->request('GET', '/.well-known/change-password');
self::assertResponseIsSuccessful();
}
public function testLogoutThrowsLogicException(): void
{
$this->expectException(\LogicException::class);
@@ -21,4 +64,20 @@ class SecurityControllerTest extends WebTestCase
$controller = new \App\Controller\SecurityController();
$controller->logout();
}
private function createUser(): User
{
$em = static::getContainer()->get(EntityManagerInterface::class);
$user = new User();
$user->setEmail('test-security-'.uniqid().'@example.com');
$user->setFirstName('Test');
$user->setLastName('User');
$user->setPassword('$2y$13$hashed');
$em->persist($user);
$em->flush();
return $user;
}
}

View File

@@ -22,6 +22,7 @@ class SitemapControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type'));
self::assertStringContainsString('/tarifs', $client->getResponse()->getContent());
}
public function testSitemapEventsReturnsXml(): void
@@ -32,4 +33,13 @@ class SitemapControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type'));
}
public function testSitemapOrgasReturnsXml(): void
{
$client = static::createClient();
$client->request('GET', '/sitemap-orgas-1.xml');
self::assertResponseIsSuccessful();
self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type'));
}
}

View File

@@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
class MessengerFailureSubscriberTest extends TestCase
{
@@ -39,6 +40,51 @@ class MessengerFailureSubscriberTest extends TestCase
$subscriber->onMessageFailed($event);
}
public function testOnMessageFailedWithRedeliveryStamp(): void
{
$em = $this->createMock(EntityManagerInterface::class);
$mailer = $this->createMock(MailerInterface::class);
$em->expects(self::once())->method('persist');
$em->expects(self::once())->method('flush');
$mailer->expects(self::once())->method('send');
$subscriber = new MessengerFailureSubscriber($em, $mailer);
$message = new \stdClass();
$envelope = new Envelope($message, [new RedeliveryStamp(3)]);
$exception = new \RuntimeException('Retry failure');
$event = new WorkerMessageFailedEvent($envelope, 'async', $exception);
$subscriber->onMessageFailed($event);
}
public function testOnMessageFailedWithNonSerializableMessage(): void
{
$em = $this->createMock(EntityManagerInterface::class);
$mailer = $this->createMock(MailerInterface::class);
$em->expects(self::once())->method('persist');
$em->expects(self::once())->method('flush');
$mailer->expects(self::once())->method('send');
$subscriber = new MessengerFailureSubscriber($em, $mailer);
$message = new class () {
public function __serialize(): array
{
throw new \RuntimeException('not serializable');
}
};
$envelope = new Envelope($message);
$exception = new \RuntimeException('Test failure');
$event = new WorkerMessageFailedEvent($envelope, 'async', $exception);
$subscriber->onMessageFailed($event);
}
public function testOnMessageFailedHandlesMailerException(): void
{
$em = $this->createMock(EntityManagerInterface::class);
@@ -55,7 +101,7 @@ class MessengerFailureSubscriberTest extends TestCase
$exception = new \RuntimeException('Test failure');
$event = new WorkerMessageFailedEvent($envelope, 'async', $exception);
$subscriber->onMessageFailed($event);
@$subscriber->onMessageFailed($event);
self::assertTrue(true);
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Tests\Repository;
use App\Entity\MessengerLog;
use App\Repository\MessengerLogRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class MessengerLogRepositoryTest extends KernelTestCase
{
public function testRepositoryIsRegistered(): void
{
self::bootKernel();
$repository = static::getContainer()->get(MessengerLogRepository::class);
self::assertInstanceOf(MessengerLogRepository::class, $repository);
}
public function testPersistAndFind(): void
{
self::bootKernel();
$em = static::getContainer()->get('doctrine.orm.entity_manager');
$log = new MessengerLog(
messageClass: 'App\Message\TestMessage',
messageBody: 'serialized',
errorMessage: 'Test error',
stackTrace: 'trace',
transportName: 'async',
retryCount: 1,
);
$em->persist($log);
$em->flush();
$found = $em->getRepository(MessengerLog::class)->find($log->getId());
self::assertNotNull($found);
self::assertSame('App\Message\TestMessage', $found->getMessageClass());
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Tests\Repository;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
class UserRepositoryTest extends KernelTestCase
{
private UserRepository $repository;
protected function setUp(): void
{
self::bootKernel();
$this->repository = static::getContainer()->get(UserRepository::class);
}
public function testUpgradePasswordUpdatesUser(): void
{
$em = static::getContainer()->get('doctrine.orm.entity_manager');
$user = new User();
$user->setEmail('test-upgrade-'.uniqid().'@example.com');
$user->setFirstName('Test');
$user->setLastName('User');
$user->setPassword('old-hash');
$em->persist($user);
$em->flush();
$this->repository->upgradePassword($user, 'new-hash');
$em->refresh($user);
self::assertSame('new-hash', $user->getPassword());
}
public function testUpgradePasswordThrowsForUnsupportedUser(): void
{
$this->expectException(UnsupportedUserException::class);
$fakeUser = new class () implements PasswordAuthenticatedUserInterface {
public function getPassword(): ?string
{
return null;
}
};
$this->repository->upgradePassword($fakeUser, 'hash');
}
}

View File

@@ -16,7 +16,7 @@ class MailerServiceTest extends TestCase
private UnsubscribeManager $unsubscribeManager;
private EntityManagerInterface $em;
private UrlGeneratorInterface $urlGenerator;
private MailerService $service;
private string $projectDir;
protected function setUp(): void
{
@@ -24,10 +24,27 @@ class MailerServiceTest extends TestCase
$this->unsubscribeManager = $this->createMock(UnsubscribeManager::class);
$this->em = $this->createMock(EntityManagerInterface::class);
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->projectDir = sys_get_temp_dir().'/mailer_test_'.uniqid();
mkdir($this->projectDir.'/public', 0o777, true);
mkdir($this->projectDir.'/config/cert', 0o777, true);
}
$this->service = new MailerService(
protected function tearDown(): void
{
@unlink($this->projectDir.'/public/key.asc');
@unlink($this->projectDir.'/config/cert/certificate.pem');
@unlink($this->projectDir.'/config/cert/private-key.pem');
@rmdir($this->projectDir.'/config/cert');
@rmdir($this->projectDir.'/config');
@rmdir($this->projectDir.'/public');
@rmdir($this->projectDir);
}
private function createService(): MailerService
{
return new MailerService(
$this->bus,
sys_get_temp_dir(),
$this->projectDir,
'passphrase',
$this->urlGenerator,
$this->unsubscribeManager,
@@ -40,7 +57,7 @@ class MailerServiceTest extends TestCase
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(true);
$this->bus->expects(self::never())->method('dispatch');
$this->service->sendEmail('user@example.com', 'Subject', '<p>Body</p>');
$this->createService()->sendEmail('user@example.com', 'Subject', '<p>Body</p>');
}
public function testSendEmailDoesNotSkipWhitelistedAddress(): void
@@ -51,7 +68,7 @@ class MailerServiceTest extends TestCase
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->service->sendEmail('contact@e-cosplay.fr', 'Subject', '<p>Body</p>');
$this->createService()->sendEmail('contact@e-cosplay.fr', 'Subject', '<p>Body</p>');
}
public function testSendEmailDispatchesForNonUnsubscribedUser(): void
@@ -63,15 +80,61 @@ class MailerServiceTest extends TestCase
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->service->sendEmail('user@example.com', 'Test', '<p>Content</p>');
$this->createService()->sendEmail('user@example.com', 'Test', '<p>Content</p>');
}
public function testSendEmailWithoutUnsubscribeHeaders(): void
{
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
$this->em->expects(self::once())->method('persist');
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->service->sendEmail('user@example.com', 'Test', '<p>Content</p>', withUnsubscribe: false);
$this->createService()->sendEmail('user@example.com', 'Test', '<p>Content</p>', withUnsubscribe: false);
}
public function testSendEmailWithReplyTo(): void
{
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
$this->unsubscribeManager->method('generateToken')->willReturn('token');
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
$this->em->expects(self::once())->method('persist');
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->createService()->sendEmail('user@example.com', 'Test', '<p>Content</p>', replyTo: 'reply@example.com');
}
public function testSendEmailWithAttachments(): void
{
$tmpFile = $this->projectDir.'/public/test.txt';
file_put_contents($tmpFile, 'test content');
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
$this->unsubscribeManager->method('generateToken')->willReturn('token');
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
$this->em->expects(self::once())->method('persist');
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->createService()->sendEmail('user@example.com', 'Test', '<p>Content</p>', attachments: [
['path' => $tmpFile, 'name' => 'test.txt'],
]);
@unlink($tmpFile);
}
public function testSendAttachesPublicKey(): void
{
file_put_contents($this->projectDir.'/public/key.asc', 'fake-pgp-key');
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
$this->unsubscribeManager->method('generateToken')->willReturn('token');
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
$this->em->expects(self::once())->method('persist');
$this->em->expects(self::once())->method('flush');
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
$this->createService()->sendEmail('user@example.com', 'Test', '<p>Content</p>');
}
}

View File

@@ -44,6 +44,27 @@ class MeilisearchServiceTest extends TestCase
self::assertFalse($this->service->indexExists('events'));
}
public function testCreateIndexIfNotExistsCreatesWhenMissing(): void
{
$this->httpClient->method('request')->willThrowException(new \RuntimeException('not found'));
$this->bus->expects(self::once())
->method('dispatch')
->with(self::callback(fn (MeilisearchMessage $m) => 'createIndex' === $m->action))
->willReturn(new Envelope(new \stdClass()));
$this->service->createIndexIfNotExists('events');
}
public function testCreateIndexIfNotExistsSkipsWhenExists(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getStatusCode')->willReturn(200);
$this->httpClient->method('request')->willReturn($response);
$this->bus->expects(self::never())->method('dispatch');
$this->service->createIndexIfNotExists('events');
}
public function testCreateIndexDispatchesMessage(): void
{
$this->bus->expects(self::once())
@@ -75,20 +96,73 @@ class MeilisearchServiceTest extends TestCase
$this->service->addDocuments('events', $docs);
}
public function testUpdateDocumentsDispatchesMessage(): void
{
$docs = [['id' => 1, 'title' => 'Updated']];
$this->bus->expects(self::once())
->method('dispatch')
->with(self::callback(fn (MeilisearchMessage $m) => 'updateDocuments' === $m->action && $m->payload['documents'] === $docs))
->willReturn(new Envelope(new \stdClass()));
$this->service->updateDocuments('events', $docs);
}
public function testDeleteDocumentDispatchesMessage(): void
{
$this->bus->expects(self::once())
->method('dispatch')
->with(self::callback(fn (MeilisearchMessage $m) => 'deleteDocument' === $m->action && 42 === $m->payload['documentId']))
->willReturn(new Envelope(new \stdClass()));
$this->service->deleteDocument('events', 42);
}
public function testDeleteDocumentsDispatchesMessage(): void
{
$ids = [1, 2, 3];
$this->bus->expects(self::once())
->method('dispatch')
->with(self::callback(fn (MeilisearchMessage $m) => 'deleteDocuments' === $m->action && $m->payload['ids'] === $ids))
->willReturn(new Envelope(new \stdClass()));
$this->service->deleteDocuments('events', $ids);
}
public function testUpdateSettingsDispatchesMessage(): void
{
$settings = ['filterableAttributes' => ['status']];
$this->bus->expects(self::once())
->method('dispatch')
->with(self::callback(fn (MeilisearchMessage $m) => 'updateSettings' === $m->action && $m->payload['settings'] === $settings))
->willReturn(new Envelope(new \stdClass()));
$this->service->updateSettings('events', $settings);
}
public function testSearchMakesPostRequest(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getStatusCode')->willReturn(200);
$response->method('toArray')->willReturn(['hits' => []]);
$this->httpClient->method('request')
->with('POST', self::stringContains('/indexes/events/search'), self::anything())
->willReturn($response);
$this->httpClient->method('request')->willReturn($response);
$result = $this->service->search('events', 'test');
self::assertArrayHasKey('hits', $result);
}
public function testGetDocumentReturnsArray(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getStatusCode')->willReturn(200);
$response->method('toArray')->willReturn(['id' => 1, 'title' => 'Event']);
$this->httpClient->method('request')->willReturn($response);
$result = $this->service->getDocument('events', 1);
self::assertSame(1, $result['id']);
}
public function testRequestReturnsEmptyArrayOn204(): void
{
$response = $this->createMock(ResponseInterface::class);

View File

@@ -79,4 +79,18 @@ class UnsubscribeManagerTest extends TestCase
$data = json_decode(file_get_contents($this->tempDir.'/var/unsubscribed.json'), true);
self::assertCount(1, $data);
}
public function testUnsubscribeCreatesDirWhenMissing(): void
{
$dir = sys_get_temp_dir().'/unsubscribe_nodir_'.uniqid();
$manager = new UnsubscribeManager($dir, 'secret');
$manager->unsubscribe('user@example.com');
self::assertTrue($manager->isUnsubscribed('user@example.com'));
@unlink($dir.'/var/unsubscribed.json');
@rmdir($dir.'/var');
@rmdir($dir);
}
}