<?php declare(strict_types=1);
namespace App\StartPlatz\Bundle\EventBundle\Controller;
/**
* CRITICAL DEPENDENCY WARNING - APPLICATION SYSTEM INTEGRATION
*
* This controller handles the event/signup REGISTRATION flow.
* It has HEAVY DEPENDENCIES on the Application system from StartupBundle.
*
* DO NOT modify Application-related code without testing ALL event registration scenarios:
* - Anonymous (non-member) event registration
* - Member event registration
* - Paid event registration with Stripe
* - Free event registration
* - Multi-batch event registration
* - Event registration with validation workflows
*
* Extracted from DefaultController (B-60 Phase 2). Uses shared services for dependencies.
*/
use App\StartPlatz\Bundle\EventBundle\Entity\Event;
use App\StartPlatz\Bundle\EventBundle\Entity\EventRepository;
use App\StartPlatz\Bundle\EventBundle\Service\EventNotificationService;
use App\StartPlatz\Bundle\EventBundle\Service\EventPaymentService;
use App\StartPlatz\Bundle\EventBundle\Service\EventRegistrationService;
use App\StartPlatz\Bundle\EventBundle\Service\EventViewHelper;
use App\StartPlatz\Bundle\MailBundle\Service\MailService;
use App\StartPlatz\Bundle\MemberBundle\Entity\Member;
use App\StartPlatz\Bundle\MemberBundle\Entity\Option;
use App\StartPlatz\Bundle\MemberBundle\Entity\Product;
use App\StartPlatz\Bundle\MemberBundle\Entity\Service;
use App\StartPlatz\Bundle\MonsumBundle\Entity\Customer;
use App\StartPlatz\Bundle\StartupBundle\Entity\Application;
use App\StartPlatz\Bundle\StartupBundle\Entity\Batch;
use App\StartPlatz\Bundle\StartupBundle\Entity\Program;
use App\StartPlatz\Bundle\StartupBundle\Form\ApplicationType;
use App\StartPlatz\Bundle\UserBundle\Entity\User;
use App\StartPlatz\Bundle\UserBundle\LoginService;
use App\StartPlatz\Bundle\MemberBundle\Entity\Team;
use App\StartPlatz\Bundle\WebsiteBundle\MenuTranslationService;
use App\StartPlatz\Bundle\WebsiteBundle\Utility\Utility;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class EventRegistrationController extends AbstractController
{
public function __construct(
private readonly SessionInterface $session,
private readonly MailService $mailService,
private readonly LoginService $loginService,
private readonly LoggerInterface $logger,
private readonly EventNotificationService $eventNotificationService,
private readonly EventPaymentService $eventPaymentService,
private readonly EventRegistrationService $eventRegistrationService,
private readonly EventViewHelper $eventViewHelper,
private readonly EntityManagerInterface $entityManager,
private readonly MenuTranslationService $menuTranslationService,
) {
}
#[Route('/signup/{slug}', name: 'signup_show_single')]
public function signupSingleAction(Request $request, $slug)
{
$em = $this->entityManager;
/** @var Program $program */
if (!$program = $em->getRepository(Program::class /*Startup Program*/)->findOneBy(['slug' => 'sign-up'])) {
$this->session->getFlashBag()->add('notice', 'ERROR Sorry, no program found');
return $this->redirect($this->generateUrl('apply_home', []));
}
/** @var Batch $batch */
if (!$batch = $em->getRepository(Batch::class)->findOneBy(['slug' => $slug, 'program' => $program])) {
$this->session->getFlashBag()->add('notice', 'ERROR Sorry, no batch found');
return $this->redirect($this->generateUrl('apply_home', []));
}
// Retrieve the UTM parameters from the URL
$utmParameters = [
'utmSource' => $request->query->get('utm_source'),
'utmMedium' => $request->query->get('utm_medium'),
'utmCampaign' => $request->query->get('utm_campaign'),
'utmTerm' => $request->query->get('utm_term'),
'utmContent' => $request->query->get('utm_content'),
];
$lang = 'DE';
if ($targetPath = $request->server->get('REDIRECT_URL')) {
$lang = $this->menuTranslationService->getLang($targetPath);
}
$batchIsOpen = $batch && $em->getRepository(Batch::class)->isBatchApplicationOpen($batch);
$form = null;
$program = null;
$member = null;
$application = null;
$product = null;
$editFeedback = "";
$settings = [];
$action = "";
$customersArr = [];
if ($batchIsOpen) {
$program = $batch->getProgram();
$settings = $this->eventViewHelper->getProgramSettings($program, $batch);
$batch = $em->getRepository(Batch::class)->updateNumbers($batch);
/** @var User $user */
if ($user = $this->getUser()) {
$member = $em->getRepository(Member::class)->find($user->getMemberId());
}
$action = $request->get('action');
$application = $this->eventRegistrationService->getOrCreateApplication($request, $batch, $program, $member, $lang, $settings, $utmParameters, null);
if ($batch->getPriceInEuroCent() > 0) {
$route = "signup_show_single";
$routeParameters["slug"] = $batch->getSlug();
$application = $this->handleAction($request, $member, $application, $batch, $lang, $route, $routeParameters, $settings);
if (is_string($application)) {
// Handle redirect
return $this->redirect($application);
}
}
if ($user && in_array($action, ["needsloginvalidation", "needsemailverification"])) {
$response = $this->validateEventApplication($application, $member, $settings, $batch, $lang);
$application = $response['data']['application'];
}
if ($batch->getPriceInEuroCent() > 0 && $batch->getProductId()) {
if (!$product = $em->getRepository(Product::class /*Product Member*/)->find($batch->getProductId())) {
$this->session->getFlashBag()->add('notice', 'ERROR configuration not found');
return $this->redirect($this->generateUrl('events_list'));
}
if ($application->getId() && isset($member) && $member->getId()) {
$customersArr = $em->getRepository(Customer::class)->findValidMonsumCustomersByMemberAndAccount($member, $product->getAccount(), false);
}
}
$form = $this->createForm(ApplicationType::class, $application, ['settings' => $settings, 'program' => $program, 'batch' => $batch]);
$form->handleRequest($request);
##############################################################
## form validation starts here ##
##############################################################
if ($form->isSubmitted() && $form->isValid()) {
$application = $em->getRepository(Application::class)->cleanApplicationData($application);
$adminEmail = $application->getEmail();
if ($member) {
$memberToBeRegistered = $member;
$isExistingMember = true;
} else {
$response = $this->eventRegistrationService->resolveOrCreateMember($application);
$isExistingMember = $response['data']['isExistingMember'];
$memberToBeRegistered = $response['data']['memberToBeRegistered'];
}
$memberToBeRegistered->setIsExistingMember($isExistingMember);
// fill application with memberId and teamId and discount information
$application = $this->eventRegistrationService->bindMemberToApplication($application, $memberToBeRegistered, $batch);
if (!$batch->isMultipleApplicationsPerMember()) {
// check if there is already an application to this event == batch with that batchId and memberId
if ($existingApplication = $em->getRepository(Application::class)->findOneBy(['batchId' => $batch->getId(), 'memberId' => $memberToBeRegistered->getId()])) {
// there are two applications - the latest application will always win
$fields = Utility::getEntityFieldsArray($application, ['id','assessments', 'votesRegistered', 'applicationStatus', 'editStatus', 'editMessage', 'lastChangeUser','history','extraFields','lastModified','createdAt']);
$storedData = Utility::fillDataByObject($existingApplication, $fields);
$updatedData = Utility::fillDataByObject($application, $fields);
$differences = array_diff_assoc($updatedData, $storedData);
$application = $existingApplication;
$application = $this->updateApplication($application, $differences);
$application->setIsExistingApplication(true);
}
}
if (isset($settings['extraFields'])) {
$application = $this->eventViewHelper->getExtraFieldsData($application, $settings, $form);
}
$dummyEvent = new Event();
$isPaidEvent = $this->eventPaymentService->isPaidEvent($application, $batch, $dummyEvent);
$hasToPay = $this->eventPaymentService->isPaymentRequired($application, $memberToBeRegistered, $dummyEvent, $settings, $batch);
if ($isPaidEvent && $hasToPay) {
// falls es bereits einen zugänglichen Monsum Account für diese Person gibt
$customersArr = $em->getRepository(Customer::class)->findValidMonsumCustomersByMemberAndAccount($memberToBeRegistered, $product->getAccount(), false);
$dummyEvent = new Event();
$application = $this->eventPaymentService->updateApplicationWithStepOneInfo($application, $memberToBeRegistered, $dummyEvent);
// Application in Datenbank schreiben, sonst kann im nächsten Schritt keine appId übergeben werden
$application = $em->getRepository(Application::class)->setApplication($application);
} else {
$route = 'signup_show_single';
$routeParameters = $request->get('_route_params');
$response = $this->eventRegistrationService->processRegistrationWithoutPayment($isExistingMember, $user, $memberToBeRegistered, $application, $settings, $lang, $adminEmail, $batch, $route, $routeParameters);
// Bei SignUps: Prüfe ob goToPage gesetzt ist
if ($batch->getGoToPage()) {
// Weiterleitung zu goToPage (kann URL oder interner Pfad sein)
return $this->redirect($batch->getGoToPage());
}
// Ansonsten auf SignUp-Seite bleiben - keine Weiterleitung
$application = $response['data']['application'];
$member = $response['data']['member'];
}
}
}
// Default to legacy template for backward compatibility
$view = '@StartPlatzEventBundle/Default/signup-single.html.twig';
// Check for pageTemplate field (new approach)
if ($batch->getPageTemplate()) {
switch ($batch->getPageTemplate()) {
case 'modern':
case 'flexible':
$view = '@StartPlatzEventBundle/Default/signup-single.modern.html.twig';
break;
case 'petition':
$view = '@StartPlatzEventBundle/Default/signup-single.deepSeekPetition.html.twig';
break;
case 'legacy':
default:
$view = '@StartPlatzEventBundle/Default/signup-single.html.twig';
break;
}
}
// Keep backward compatibility for existing petition SignUps using JSON settings
elseif (array_key_exists('signupTemplate', $settings)) {
$view = '@StartPlatzEventBundle/Default/signup-single.deepSeekPetition.html.twig';
}
// Build the metaData array using only the $batch object (landing page fields)
$metaData = [
// SEO Tag: Description (using landingPageDescription)
'description' => $batch->getLandingPageDescription(),
// Canonical URL (the domain remains lowercase)
'canonical' => 'https://www.startplatz.de/signup/' . urlencode($batch->getSlug()),
// Open Graph Tags for Social Sharing (using landingPage fields)
'ogTitle' => $batch->getLandingPageTitle(),
'ogDescription' => $batch->getLandingPageDescription(),
'ogImage' => $batch->getLandingPageSocialImage(),
'ogUrl' => 'https://www.startplatz.de/signup/' . urlencode($batch->getSlug()),
'ogType' => 'website',
'ogSiteName' => 'STARTPLATZ', // Protected trademark must be uppercase
// Twitter Card Tags (using landingPage fields)
'twitterCard' => 'summary', // or "summary_large_image" if preferred
'twitterTitle' => $batch->getLandingPageTitle(),
'twitterDescription'=> $batch->getLandingPageDescription(),
'twitterImage' => $batch->getLandingPageSocialImage(),
'twitterSite' => '@STARTPLATZ', // Protected trademark must be uppercase
// Title for the page (taken from landingPageTitle)
'title' => $batch->getLandingPageTitle(),
// Additional Landing Page Fields
'landingPageCss' => $batch->getLandingPageCss(),
'landingPageContent' => $batch->getLandingPageContent(),
'landingPageJs' => $batch->getLandingPageJs(),
'landingPageFooterTemplate' => $batch->getLandingPageFooterTemplate(),
];
return $this->render($view, [
'batch' => $batch,
'program' => $program,
'settings' => $settings,
'metaData' => $metaData,
'form' => $form ? $form->createView() : $form,
'application' => $application,
'action' => $action,
'batchIsOpen' => $batchIsOpen,
'customersArr' => $customersArr,
'member' => $member,
'isMultiBatchEvent' => false, // Signups have no event, so no multi-batch
'utmParameters' => $utmParameters,
'phrases' => $em->getRepository(Option::class)->getApplicationPhrases($program, $batch, false, $lang),
'editFeedback' => $editFeedback,
'templateVars' => [],
'preview' => $request->get('preview'),
'menuLinksAndPhrases' => $this->menuTranslationService->getMenuLinksAndPhrases($request->server->get('REQUEST_URI')),
'lang' => $lang,
'targetRoute' => "signup_show_single",
'targetRouteSlug' => $batch->getSlug(),
'targetPath' => $targetPath,
]);
}
### section signup ends here ###
#[Route('/allmeda/event/application/send-confirmation-mail/{applicationId}', name: 'allmeda_event_send-confirmation-mail')]
/**
* @IsGranted("ROLE_ADMIN")
*/
public function sendConfirmationMailViaPublicFunction($applicationId)
{
$em = $this->entityManager;
$application = $em->getRepository(Application::class)->find($applicationId);
$member = $em->getRepository(Member::class)->find($application->getMemberId());
$batch = $em->getRepository(Batch::class)->find($application->getBatchId());
$feedback = $this->eventNotificationService->sendConfirmationMail(null, $member, [], $application, $batch);
$response = "SUCCESS Confirmation Mail has been sent";
return new Response($response);
}
#[Route('/event-confirmation/{slug}/', name: 'event_confirmation')]
public function eventConfirmationAction(Request $request, $slug)
{
$em = $this->entityManager;
$event = $this->findEventBySlug($slug);
if (!$event) {
$this->addFlash('notice', 'ERROR Event not found');
return $this->redirectToRoute('events_list');
}
$seriesEvents = [];
if ($seriesTag = $event->getSeriesTag()) {
/** @var EventRepository $eventRepository */
$eventRepository = $em->getRepository(Event::class);
$seriesEvents = $eventRepository->findBySeriesTag($seriesTag, '', ['startDate' => 'DESC']);
}
$embed = $request->get('embed');
$batch = $em->getRepository(Batch::class)->findOneBy(['programId'=>27, 'eventId'=>$event->getId(), 'status'=>'active', 'access' =>'anonymous']);
if (!$batch) {
$this->addFlash('notice', 'ERROR No active batch found for this event');
return $this->redirectToRoute('events_list');
}
$program = $batch->getProgram();
$action = $request->get('action');
$applicationId = $request->get('app');
$route = "event_confirmation";
$routeParameters["slug"] = $event->getSlug();
$settings = [];
$member = false;
$application = new Application();
// Load application first, then determine language
if ($applicationId){
if (!$application = $em->getRepository(Application::class)->find($applicationId)){
$this->addFlash('notice', 'ERROR Application with ID '.$applicationId.' not found');
return $this->redirectToRoute('events_list');
}
}
// Language detection: Priority is Application > Event > Default DE
// This ensures user's language choice persists through Stripe redirect
$lang = ($application && $application->getId() && $application->getLang())
? $application->getLang()
: ($event->getLang() ?? 'DE');
// Handle action after language is determined
if ($applicationId && $action){
$application = $this->handleAction($request, $member, $application, $batch, $lang, $route, $routeParameters, $settings);
if (is_string($application)) {
// Handle redirect
return $this->redirect($application);
}
}
// Generate the absolute base URL based on the route and parameters
$baseURL = $this->generateUrl('event_show_single', ['slug' => $slug], UrlGeneratorInterface::ABSOLUTE_URL);
// UTM parameters for the promotion URL
$promotionUtmParameters = [
'utm_source' => 'affiliate',
'utm_medium' => is_numeric($application->getMemberId()) ? $application->getMemberId() : '',
'utm_campaign' => 'event:' . $event->getId(),
'utm_term' => '',
'utm_content' => '',
];
$promotionUrl = $em->getRepository(Batch::class)->generatePromotionUrl($baseURL, $promotionUtmParameters);
$view = '@StartPlatzEventBundle/Default/event-confirmation.html.twig';
if ($embed){
$view = '@StartPlatzEventBundle/Default/event-confirmation.embed.html.twig';
}
if ($event->getTwigFile() == 'winter-2025' ){
$view = '@StartPlatzEventBundle/Default/event-confirmation.winter-2025.html.twig';
}
if ($event->getTwigFile() == 'style-ecodynamics' ){
$view = '@StartPlatzEventBundle/Default/event-confirmation.style-ecodynamics.html.twig';
}
if ($event->getTwigFile() == 'style-ai-hub' ){
$view = '@StartPlatzEventBundle/Default/event-confirmation.style-ai-hub.html.twig';
}
if ($event->getTwigFile() == 'style-startplatz' ){
$view = '@StartPlatzEventBundle/Default/event-confirmation.style-ai-hub.html.twig';
}
return $this->render($view, [
'event' => $event,
'seriesEvents' => $seriesEvents,
'promotionUrl' => $promotionUrl,
'batch' => $batch,
'settings' => $settings,
'application' => $application,
'action' => $action,
'member' => $member,
'speakers' => $this->eventViewHelper->getSpeakers($event),
'phrases' => $em->getRepository(Option::class)->getApplicationPhrases($program, $batch, false, $lang),
'templateVars' => [],
'embed' => $embed,
'menuLinksAndPhrases' => $this->menuTranslationService->getMenuLinksAndPhrases($request->server->get('REQUEST_URI')),
'lang' => $lang,
'isMultiBatchEvent' => $event->isMultiBatchEvent(),
]);
}
#[Route('/event/{slug}/', name: 'event_show_single')]
#[Route('/event/{slug}/{date}', name: 'event_show_single_by_date', requirements: [])]
public function eventSingleAction(Request $request, $slug, $date = null)
{
##event-single
if ($slug == 'ai-bootcamp-koeln-2024-01-18') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-bootcamp-koeln-2024-01-29')) );
if ($slug == 'ai-summer-school-koeln-2208') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-bootcamp-koeln-1109')) );
if ($slug == 'ai-summer-school-duesseldorf-2908') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-bootcamp-duesseldorf-2609')) );
if ($slug == 'ai-disruptme-marketing-in-unternehmen-koeln-2023-10-19') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-disruptme-marketing-in-unternehmen-koeln-2024-01-31')) );
if ($slug == 'ai-disruptme-publisher-koeln-2023-10-18') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-disruptme-publisher-duesseldorf-2024-03-20')) );
if ($slug == 'ai-disruptme-travel-tourismus-duesseldorf-2023-11-09') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-disruptme-travel-tourismus-duesseldorf-2024-02-22')) );
if ($slug == 'genai-ki-app-no-code-backend-fuer-ki-anwendung-2024-05-28') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'genai-ki-anwendung-bauen-frontend-ux-ui-2024-05-21')) );
if ($slug == 'ai-barcamp-generative-ki') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ai-barcamp-logistik')) );
if ($slug == 'genai-generative-ki-zertifikat-ki-kompetenz-erwerben-2025-05-20') return $this->redirect( $this->generateUrl('event_show_single', array('slug' => 'ki-kompetenzpflicht-zertifikat-artikel-4-2025-05-20')) );
// Retrieve the UTM parameters from the URL
$utmParameters = [
'utmSource' => $request->query->get('utm_source') ?? 'homepage',
'utmMedium' => $request->query->get('utm_medium'),
'utmCampaign' => $request->query->get('utm_campaign'),
'utmTerm' => $request->query->get('utm_term'),
'utmContent' => $request->query->get('utm_content'),
];
$em = $this->entityManager;
/** @var Event $event */
$event = $this->findEventBySlug($slug);
if (!$event) {
$this->addFlash('notice', 'ERROR Veranstaltung nicht gefunden');
return $this->redirectToRoute('events_list');
}
// Handle event visibility: redirect if not published or previewed without user session.
$response = $this->handleEventStatus($event, $request);
if ($response) {
return $response;
}
$seriesEvents = [];
if ($seriesTag = $event->getSeriesTag()) {
/** @var EventRepository $eventRepository */
$eventRepository = $em->getRepository(Event::class);
$seriesEvents = $eventRepository->findBySeriesTag($seriesTag, '', ['startDate' => 'DESC']);
}
// Language detection with priority: URL parameter > Event default
// This ensures ?lang=en parameter works for bilingual events
$lang = strtoupper($request->query->get('lang', '')) ?: ($event->getLang() ?? 'DE');
$embed = $request->get('embed');
$eventbriteId = $event->getEventbriteId();
$isEventbriteOnly = $event->getTicketing() > '';
$applicationUrl = $event->getApplicationUrl();
// Multi-batch aware logic
$futureBatches = [];
$allBatches = [];
if ($event->isMultiBatchEvent()) {
$allBatches = $em->getRepository(Batch::class)->findAllBatchesByEvent($event);
if ($date) {
// User selected a specific date
$batch = $em->getRepository(Batch::class)->findBatchByEventAndDate($event, $date);
if (!$batch) {
// Invalid date, redirect to main event page
$this->addFlash('notice', 'Der gewählte Termin ist nicht verfügbar.');
return $this->redirectToRoute('event_show_single', ['slug' => $slug]);
}
} else {
// No date selected, show next upcoming batch
$batch = $em->getRepository(Batch::class)->findNextOpenBatchByEvent($event);
}
// Get all future batches for the selector
$futureBatches = $em->getRepository(Batch::class)->findFutureBatchesByEvent($event, 10);
// Get past batches for archive display
$pastBatches = $em->getRepository(Batch::class)->findPastBatchesByEvent($event, 10);
} else {
// Single batch event - keep existing logic
// Try to find active batch first, then fall back to completed batch for display
$batch = $em->getRepository(Batch::class)->findOneBy(['programId'=>27, 'eventId'=>$event->getId(), 'status'=>'active', 'access' =>'anonymous']);
if (!$batch) {
// No active batch found, look for completed batch (for past events that need to display)
$batch = $em->getRepository(Batch::class)->findOneBy(['programId'=>27, 'eventId'=>$event->getId(), 'status'=>'completed', 'access' =>'anonymous']);
}
if ($batch) {
$allBatches = [$batch];
}
}
$batchIsOpen = $batch && $em->getRepository(Batch::class)->isBatchApplicationOpen($batch);
$form = null;
$program = null;
$application = null;
$product = null;
$editFeedback = "";
$settings = [];
$action = "";
$customersArr = [];
$member = null;
/** @var User $user */
if ($user = $this->getUser()) {
$member = $em->getRepository(Member::class)->find($user->getMemberId());
}
if ($batch && !$isEventbriteOnly) {
$program = $batch->getProgram();
}
if ($batchIsOpen && !$isEventbriteOnly) {
$settings = $this->eventViewHelper->getProgramSettings($program, $batch);
$batch = $em->getRepository(Batch::class)->updateNumbers($batch);
$action = $request->get('action');
$referral = $request->get('referral');
// do some exceptions
if ($member && isset($settings['batchType'])) {
if ($settings['batchType'] == 'memberDocumentation'){
// check if member
if ($em->getRepository(Application::class)->findApplicationsByAiFridayBatchSlugAndMemberId($member->getId()))
{
if (!$em->getRepository(Application::class)->findOneBy(['memberId'=>$member->getId(), 'batchId' => $batch->getId()])){
$memberTeam = $em->getRepository(Team::class)->find($member->getTeamId());
$adminEmail = $member->getEmail();
$application = $em->getRepository(Application::class)->createApplicationIfNotExists($program, $batch, $memberTeam, $member, $adminEmail);
$application->setBatchStatus('applicant');
$application->setApplicationStatus('applied');
$em->persist($application);
$em->flush();
$batch = $em->getRepository(Batch::class)->updateNumbers($batch);
}
}
}
}
$application = $this->eventRegistrationService->getOrCreateApplication($request, $batch, $program, $member, $lang, $settings, $utmParameters, $event);
if ($referral && $application->getRealPriceinEuroCent() > 0) {
if ($recommendedByMember = $em->getRepository(Member::class)->find($referral)) {
if ($neededTag = $settings['isRecommendedBy'] ?? '') {
if (Utility::contains($recommendedByMember->getTags(), $neededTag)) {
$application->setRealPriceinEuroCent(0);
$application->setDiscountReason("geworben durch {$recommendedByMember->getName()}");
$application->setDiscountPercent(100);
}
}
}
}
if ($user && in_array($action, ["needsloginvalidation", "needsemailverification"])) {
$response = $this->validateEventApplication($application, $member, $settings, $batch, $lang);
$application = $response['data']['application'];
}
// Only check for Monsum product if we're not using Stripe
$hasStripeConfig = ($batch && $batch->getStripePriceId()) || ($event && $event->getStripePriceId());
if ($batch->getPriceInEuroCent() > 0 && $batch->getProductId() && !$hasStripeConfig) {
if (!$product = $em->getRepository(Product::class /*Product Member*/)->find($batch->getProductId())) {
$this->session->getFlashBag()->add('notice', 'ERROR configuration not found');
return $this->redirect($this->generateUrl('events_list'));
}
}
$form = $this->createForm(ApplicationType::class, $application, [
'settings' => $settings,
'program' => $program,
'batch' => $batch,
'csrf_protection' => $embed ? false : true,
]);
$form->handleRequest($request);
##############################################################
## form validation starts here ##
##############################################################
if ($form->isSubmitted() && $form->isValid()) {
$application = $em->getRepository(Application::class)->cleanApplicationData($application);
$adminEmail = $application->getEmail();
$email = $application->getEmail();
if (!Utility::validateEmail($email)){
$this->addFlash('notice', 'ERROR Incorrect email adress. Please check your email adress!');
} else {
// maxApplicants
if (isset($settings['maxApplicants'])) {
$applicants = $em->getRepository(Application::class)->findBy(['batchId' => $batch->getId(), 'applicationStatus' => 'applied']);
$currentApplicantsCount = count($applicants);
if ($currentApplicantsCount >= $settings['maxApplicants']) {
$this->addFlash('notice', 'ERROR Leider sind alle verfügbaren Plätze bereits vergeben.');
return $this->redirectToRoute('event_show_single', ['slug' => $slug]);
}
}
// isErrorDetectionOn
if (isset($settings['isErrorDetectionOn']) and $settings['isErrorDetectionOn']) {
$this->addFlash('notice', 'ERROR There was a problem in processing your application. Please contact support@startplatz.de');
return $this->redirectToRoute('event_show_single', ['slug' => $slug]);
}
if ($member) {
$memberToBeRegistered = $member;
$isExistingMember = true;
} else {
$response = $this->eventRegistrationService->resolveOrCreateMember($application);
$isExistingMember = $response['data']['isExistingMember'];
$memberToBeRegistered = $response['data']['memberToBeRegistered'];
}
$memberToBeRegistered->setIsExistingMember($isExistingMember);
// fill application with memberId and teamId and discount information
$application = $this->eventRegistrationService->bindMemberToApplication($application, $memberToBeRegistered, $batch);
if (!$batch->isMultipleApplicationsPerMember()) {
// check if there is already an application to this event == batch with that batchId and memberId
if ($existingApplication = $em->getRepository(Application::class)->findOneBy(['batchId' => $batch->getId(), 'memberId' => $memberToBeRegistered->getId()])) {
// there are two applications - the latest application will always win
$fields = Utility::getEntityFieldsArray($application, ['id', 'assessments', 'votesRegistered', 'applicationStatus', 'editStatus', 'editMessage', 'lastChangeUser', 'history', 'extraFields', 'lastModified', 'createdAt']);
$storedData = Utility::fillDataByObject($existingApplication, $fields);
$updatedData = Utility::fillDataByObject($application, $fields);
$differences = array_diff_assoc($updatedData, $storedData);
$application = $existingApplication;
$application = $this->updateApplication($application, $differences);
$application->setIsExistingApplication(true);
}
}
if (isset($settings['extraFields'])) {
$application = $this->eventViewHelper->getExtraFieldsData($application, $settings, $form);
}
$isPaidEvent = $this->eventPaymentService->isPaidEvent($application, $batch, $event);
// Debug logging for ki-campus payment exception
$this->logger->info('Payment exception check', [
'member_exists' => $member ? true : false,
'member_id' => $member ? $member->getId() : null,
'member_email' => $member ? $member->getEmail() : null,
'member_tags' => $member ? $member->getTags() : null,
'memberToBeRegistered_id' => $memberToBeRegistered->getId(),
'memberToBeRegistered_email' => $memberToBeRegistered->getEmail(),
'memberToBeRegistered_tags' => $memberToBeRegistered->getTags(),
'batch_isFreeForKiCampus' => $batch ? $batch->getIsFreeForKiCampus() : null,
'event_isFreeForKiCampus' => $event ? $event->getIsFreeForKiCampus() : null,
]);
// Always use memberToBeRegistered for payment exception check
$hasToPay = $this->eventPaymentService->isPaymentRequired($application, $memberToBeRegistered, $event, $settings, $batch);
if ($isPaidEvent && $hasToPay) {
$application = $this->eventPaymentService->updateApplicationWithStepOneInfo($application, $memberToBeRegistered, $event);
$isAdmin = $user && $user->getIsAdmin();
$paymentLink = $this->eventPaymentService->createStripeCheckoutUrl($application, $memberToBeRegistered, $event, $batch, $embed, $isAdmin);
if (is_string($paymentLink)) {
// call stripe and let applicant pay for the ticket
if ($embed){
return $this->render('@StartPlatzEventBundle/Default/iframe_redirect.html.twig', [
'targetUrl' => $paymentLink
]);
} else {
return $this->redirect($paymentLink);
}
} elseif ($paymentLink === null) {
// No Stripe config — check Monsum
if ($product) {
$customersArr = $em->getRepository(Customer::class)->findValidMonsumCustomersByMemberAndAccount($memberToBeRegistered, $product->getAccount(), false);
}
}
// paymentLink === false: Stripe was configured but failed — fall through to render page
// Application in Datenbank schreiben, sonst kann im nächsten Schritt keine appId übergeben werden
$application = $em->getRepository(Application::class)->setApplication($application);
} else {
$route = 'event_show_single';
$routeParameters = $request->get('_route_params');
if (!array_key_exists('slug', $routeParameters)) {
$routeParameters['slug'] = $event->getSlug();
}
$response = $this->eventRegistrationService->processRegistrationWithoutPayment($isExistingMember, $user, $memberToBeRegistered, $application, $settings, $lang, $adminEmail, $batch, $route, $routeParameters);
if ($response['status'] == 'error') {
$targetPath = $response['data']['targetPath'];
return $this->redirect($targetPath);
}
$application = $response['data']['application'];
$member = $response['data']['member'];
if ($this->shouldSetAiHubMembership($settings, $application->getApplicationStatus())) {
$aiHubStatus = $settings["setAiHubMembership"];
$service = $this->configureServiceForAiHub($aiHubStatus, $event->getStartDate());
$reason = "participated in event with id={$event->getId()} and title={$event->getTitle()}";
$em->getRepository(Member::class)->setAiHubMembership($service, $member, $reason);
}
}
}
}
}
$promotionUrl = null;
if ($user && $batch) {
// Generate the absolute base URL based on the route and parameters
$baseURL = $this->generateUrl('event_show_single', ['slug' => $slug], UrlGeneratorInterface::ABSOLUTE_URL);
// UTM parameters for the promotion URL
$promotionUtmParameters = [
'utm_source' => 'affiliate',
'utm_medium' => $member->getId(),
'utm_campaign' => 'event:' . $event->getId(),
'utm_term' => '',
'utm_content' => '',
];
$promotionUrl = $em->getRepository(Batch::class)->generatePromotionUrl($baseURL, $promotionUtmParameters);
}
// Überprüfen Sie den Wert des Teaserfeldes, um showTeaser zu setzen
$showTeaser = !empty($event->getTeaser());
$paymentLink = false;
$view = '@StartPlatzEventBundle/Default/event-single.html.twig';
if ($event->isSinglePage() or $embed){
$view = '@StartPlatzEventBundle/Default/event-single.lp.html.twig';
}
if ($event->getTwigFile() == 'winter-2025' ){
$view = '@StartPlatzEventBundle/Default/event-single.winter-2025.html.twig';
}
if ($event->getTwigFile() == 'style-ecodynamics' ){
$view = '@StartPlatzEventBundle/Default/event-single.style-ecodynamics.html.twig';
}
if ($event->getTwigFile() == 'style-ai-hub' ){
$view = '@StartPlatzEventBundle/Default/event-single.style-ai-hub.html.twig';
}
if ($event->getTwigFile() == 'style-startplatz' ){
$view = '@StartPlatzEventBundle/Default/event-single.style-startplatz.html.twig';
}
if ($event->getTwigFile() == 'tailwind' ){
$view = '@StartPlatzEventBundle/Default/event-single.tailwind.html.twig';
}
return $this->render($view, [
'event' => $event,
'seriesEvents' => $seriesEvents,
'showTeaser' => $showTeaser,
'utmParameters' => $utmParameters,
'promotionUrl' => $promotionUrl,
'eventbriteId' => $eventbriteId,
'isEventbriteOnly' => $isEventbriteOnly,
'applicationUrl' => $applicationUrl,
'batch' => $batch,
'settings' => $settings,
'form' => $form ? $form->createView() : $form,
'application' => $application,
'action' => $action,
'batchIsOpen' => $batchIsOpen,
'customersArr' => $customersArr,
'member' => $member,
'paymentLink' => $paymentLink,
// Multi-batch event support
'futureBatches' => $futureBatches,
'pastBatches' => $pastBatches ?? [],
'allBatches' => $allBatches,
'selectedDate' => $date,
'isMultiBatchEvent' => $event->isMultiBatchEvent(),
'speakers' => $this->eventViewHelper->getSpeakers($event),
'phrases' => $em->getRepository(Option::class)->getApplicationPhrases($program, $batch, false, $lang),
'editFeedback' => $editFeedback,
'templateVars' => [],
'preview' => $request->get('preview'),
'menuLinksAndPhrases' => $this->menuTranslationService->getMenuLinksAndPhrases($request->server->get('REQUEST_URI')),
'lang' => $lang,
'isEnglish' => $lang === 'EN',
'embed' => $embed,
'targetRoute' => "event_show_single",
'targetRouteSlug' => $event->getSlug(),
]);
}
// ── Private Helpers ──────────────────────────────────────────────
private function findEventBySlug($slug)
{
/** @var EventRepository $eventRepository */
$eventRepository = $this->entityManager->getRepository(Event::class);
$event = $eventRepository->findOneBy(['slug' => $slug]);
if (!$event) {
$querySlug = '/' . $slug;
$event = $eventRepository->findOneBy(['slug' => $querySlug]);
if (!$event) {
return false;
} else {
$em = $this->entityManager;
$event->setSlug($slug);
$em->persist($event);
$em->flush();
}
}
return $event;
}
private function handleEventStatus(Event $event, Request $request)
{
// SEO-optimized handling: Archive events are displayed normally (no redirect)
// Template will show archive banner and hide registration forms
// This preserves 100% SEO juice and backlinks
if ($event->getStatus() != 'publish' and $event->getStatus() != 'archive' and !$request->get('preview')) {
// Draft and trash events require authentication
if (!$this->getUser()) {
$this->addFlash('notice', 'ERROR Veranstaltung ist nicht verfügbar');
return $this->redirectToRoute('events_list');
}
}
return null;
}
/**
* handleAction - Handles various actions for the application
*
* This function is called in the following contexts:
* 1. During Signup (signupSingleAction)
* - With route "signup_show_single"
* - Parameters include batch slug, language, etc.
*
* 2. During Event Handling (eventSingleAction / eventConfirmationAction)
* - With route "event_show_single" or "event_confirmation"
* - Parameters include event slug, language, etc.
*/
private function handleAction(Request $request, $member, Application $application, Batch $batch, $lang, $route, $routeParameters, $settings)
{
$action = $request->get("action");
$em = $this->entityManager;
switch ($action) {
case "checkSuccess":
$provider = $request->get('provider');
if ($provider == 'stripe'){
$application->setEditStatus("success:stripePaid");
}
$application->setApplicationStatus('applied');
$application->setHasPaid(true);
$application = $em->getRepository(Application::class)->setApplication($application);
switch ($lang) {
case "DE":
$message = "Danke für Deine Anmeldung.<br>Wir freuen uns auf Dich!";
break;
case "EN":
$message = "Thanks, Your application is now approved.<br>We are looking forward to having you!";
break;
}
if ($productNumber = $request->get('productnumber') or $provider == 'stripe'){
## productNumber is only given in case of sign up
switch ($lang) {
case "DE":
$message = "Danke für Deine Buchung.<br>Wir freuen uns auf Dich!";
break;
case "EN":
$message = "Thanks, Your booking was successful.<br>We are looking forward to having you!";
break;
}
$account = $request->get('account');
$customerHash = $request->get('customerId');
$subscriptionHash = $request->get('subscriptionId');
if (!$member) {
$member = $em->getRepository(Member::class)->find($application->getMemberId());
}
if (array_key_exists("setAiHubMembership", $settings)) {
// Hole den Status aus den Einstellungen
$aiMembershipStatus = $settings["setAiHubMembership"];
// Setze das Start- und Enddatum basierend auf dem aktuellen Datum und dem Status
$aiMembershipStartDate = new DateTime(); // Setze auf aktuelles Datum und Uhrzeit
$aiMembershipEndDate = clone $aiMembershipStartDate;
if ($aiMembershipStatus === 'trial') {
$aiMembershipEndDate->modify('+1 month');
} elseif ($aiMembershipStatus === 'active') {
$aiMembershipEndDate->modify('+3 months');
}
// Setze den Grund für die Mitgliedschaft
$reason = "Buy of {$productNumber}";
// Erstelle oder aktualisiere das Service-Objekt mit den neuen Daten
// (Du müsstest diese Logik entsprechend deiner Anwendung implementieren)
$service = new Service(); // oder hole ein existierendes Service-Objekt
$service->setStatus($aiMembershipStatus);
$service->setStartDate($aiMembershipStartDate);
$service->setEndDate($aiMembershipEndDate);
// Rufe die setAiHubMembership Funktion auf
$successContent[] = $em->getRepository(Member::class)->setAiHubMembership($service, $member, $reason);
}
$loginLink = null;
$targetPath = $this->generateUrl('x_membership_first-steps', ['productNumber' => $productNumber, 'account' => $account]);
$this->session->getFlashBag()->add('notice', $message);
if (!$user = $this->getUser()) {
$loginLink = $this->loginService->getLoginLinkByTargetPath($member->getEmail(), $targetPath);
$user = $em->getRepository(User::class)->findOneBy(['memberId'=>$member->getId()]);
}
$feedback = $this->eventNotificationService->sendConfirmationMail($user, null, $settings, $application, $batch);
if ($provider == 'stripe'){
$mailContent = [
'applicationId' => $application->getId(),
'memberId' => $application->getMemberId(),
'editStatus' => $application->getEditStatus(),
'applicationStatus' => $application->getApplicationStatus(),
'person' => $application->getPerson(),
'email' => $application->getEmail(),
'utmSource' => $application->getUtmSource(),
'batchSlug' => $application->getBatchSlug(),
];
$response = $this->mailService->send("stripe step two {$application->getBatchId()} {$application->getEmail()}", 'support@startplatz.de', 'Monitoring Stripe', 'lorenz.graef@startplatz.de', 'Lorenz', print_r($mailContent,true), false);
return $application;
} else {
return isset($loginLink) ? $loginLink : $targetPath;
}
} else { // if productNumber is not defined
if (!$user = $this->getUser()) {
$user = $em->getRepository(User::class)->findOneBy(['memberId'=>$member->getId()]);
}
$feedback = $this->eventNotificationService->sendConfirmationMail($user, null, $settings, $application, $batch);
}
break;
case "sendMagicLinkForPayment":
// create LoginLink and send email
$customerId = $request->get('customerId');
$feedbackUrl = $this->eventPaymentService->sendMagicLinkForPayment($application, $batch, $customerId, $route, $routeParameters);
return $feedbackUrl;
case "createConsumerMonsumAccount":
case "useExistingCustomerAccount":
$customerId = $request->get('customerId');
$checkoutUrl = $this->eventPaymentService->prepareMonsumCheckout($application, $batch, $action, $route, $routeParameters, $customerId);
return $checkoutUrl;
}
// Return the modified $application object to the main controller
return $application;
}
private function validateEventApplication(Application $application, Member $member, $settings, $batch, string $lang): array
{
$em = $this->entityManager;
$message = "";
$status = "";
// Check if a confirmation email needs to be sent
$shouldSendConfirmationMail = false;
$feedback = null;
switch ($application->getEditStatus()) {
case "error:validateLogin":
$application->setEditStatus('success:validatedLogin');
$application->setEditMessage("Application has been validated by {$member->getEmail()}");
$application = $em->getRepository(Application::class)->setHistory($application, $application->getEditMessage());
$application = $em->getRepository(Application::class)->setApplication($application, $member);
$status = "success";
$shouldSendConfirmationMail = true;
break;
case "error:validateEmail":
$application->setEditStatus('success:validatedEmail');
$application->setEditMessage("Email of this Application has been validated by {$member->getEmail()}");
$application = $em->getRepository(Application::class)->setHistory($application, $application->getEditMessage());
if ($application->getApplicationStatus() == 'started') {
$application->setApplicationStatus('applied');
}
$application = $em->getRepository(Application::class)->setApplication($application, $member);
$status = "success";
$shouldSendConfirmationMail = true;
break;
}
if ($shouldSendConfirmationMail) {
// Logic to send confirmation mail
$feedback = $this->eventNotificationService->sendConfirmationMail(null, $member, $settings, $application, $batch);
}
if ($status == "success") {
$application = $em->getRepository(Application::class)->setApplicationApplied($application, $member);
switch ($lang) {
case "DE":
$message = "SUCCESS Danke, Deine Anmeldung ist jetzt bestätigt. Wir freuen uns auf Dich!";
if ($feedback){
$message .= "<br>Wir haben Dir eine Bestätigungsmail geschickt. Bitte prüfe Deinen Posteingang.";
}
break;
case "EN":
$message = "SUCCESS Thanks, Your application is now approved. We are looking forward to having you!";
if ($feedback){
$message .= "<br>We have sent you a confirmation mail. Please check your inbox!";
}
break;
}
$this->session->getFlashBag()->add('notice', $message);
}
return [
'data' => [
'application' => $application,
],
'message' => $message,
'status' => $status,
];
}
private function shouldSetAiHubMembership($settings, $applicationStatus)
{
return array_key_exists("setAiHubMembership", $settings) && $applicationStatus === 'applied';
}
private function configureServiceForAiHub($status, DateTime $startDate)
{
$service = new Service(); // oder hole ein existierendes Service-Objekt
$endDate = clone $startDate;
if ($status === 'trial') {
$endDate->modify('+1 month');
} elseif ($status === 'active') {
$endDate->modify('+3 months');
}
$service->setStatus($status);
$service->setStartDate($startDate);
$service->setEndDate($endDate);
return $service;
}
private function updateApplication(Application $application, $differences)
{
foreach ($differences as $diff => $value) {
$method = 'set' . ucfirst((string) $diff);
if (method_exists($application, $method)) {
$application->$method($value);
}
}
return $application;
}
}