<?php declare(strict_types=1);
namespace App\StartPlatz\Bundle\StartupBundle\Repository;
use App\StartPlatz\Bundle\GruendungsstipendiumBundle\Entity\GsJuryAssessment;
use App\StartPlatz\Bundle\MemberBundle\Entity\Team;
use App\StartPlatz\Bundle\MetaBundle\Entity\Attribute;
use App\StartPlatz\Bundle\MetaBundle\Entity\Tag;
use App\StartPlatz\Bundle\StartupBundle\Entity\Batch;
use App\StartPlatz\Bundle\StartupBundle\Entity\Program;
use App\StartPlatz\Bundle\StartupBundle\Entity\StartupRelevance;
use DateTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use App\StartPlatz\Bundle\StartupBundle\Entity\Application;
use App\StartPlatz\Bundle\StartupBundle\Entity\Startup;
use App\StartPlatz\Bundle\StartupBundle\Entity\StartupAttribute;
use App\StartPlatz\Bundle\UserBundle\Entity\User;
use App\StartPlatz\Bundle\WebsiteBundle\Utility\Utility;
use Doctrine\Persistence\ManagerRegistry;
/**
* StartupRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class StartupRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $managerRegistry)
{
parent::__construct($managerRegistry, Startup::class);
}
//Connect
public function find4SearchConnectStartup()
{
$connection = $this->getEntityManager()->getConnection();
$sql ="SELECT t.name, t.previousName, t.status, t.shortName, t.id, s.tags, s.slug
FROM sp_startup_profiles s, sp_teams t
WHERE s.teamId = t.id and s.visibility IN ('sp-connect', 'public')
";
return $connection->fetchAllAssociative($sql, []);
}
public function synchronizeStartupAndTeamEntities($criteria, $order = [])
{
$qb = $this->getEntityManager()->createQueryBuilder();
$responseContent = [];
$qb->select('s.id as startupId, s.teamId as startupTeamId, t.id as teamId')
->from(Startup::class, 's')
->innerJoin(Team::class, 't', 'with', 's.teamId = t.id')
->where('s.teamId != t.id')
;
if ($criteria) {
foreach($criteria as $field =>$value) {
if ($field == 'categories' or $field == 'tags') {
$qb->andWhere('s.'. $field . " LIKE '%" . $value . "%'");
} elseif ($value == 'IS NULL' or $value == null) {
$qb->andWhere('s.'. $field . " IS NULL");
} elseif (is_array($value)) {
$qb->andWhere('s.'. $field . " IN (:{$field})");
$qb->setParameter($field, array_keys($value));
} else {
$qb->andWhere('s.'. $field . ' = :' . $field);
$qb->setParameter($field, $value);
}
}
}
$items = $qb->getQuery()->getResult();
foreach ($items as $item) {
$responseContent[] = $this->setField('teamId', $item['teamId'], $item['startupId']);
}
return $responseContent;
}
/**
* Check if a team is in the current batch.
*
* @deprecated UP-024: The 'startplatz' field was removed from Startup entity.
* This method now returns false. The concept of "currentBatch" needs
* to be reimplemented via Participation entity if still needed.
*/
public function isCurrentBatch(Team $team): bool
{
// UP-024: The 'startplatz' field was removed from Startup.
// Previously this checked for startplatz='currentBatch'.
// TODO: Reimplement via Participation entity if this functionality is still needed.
return false;
}
public function findFilterMultiple($criteria, $field, $delimiter)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's.'. $field
);
$qb->from(Startup::class, 's')
->where('s.'. $field . " > '' ")
;
$qb = $this->setCriteria($qb, $criteria);
$qb->orderBy('s.name', 'ASC');
$entities = $qb->getQuery()->getResult();
if ($field == 'todos') {
return Utility::countJsonTags($entities, $field, $delimiter);
} else {
return Utility::countTags($entities, $field, $delimiter);
}
}
public function transferStartupIdToWinner($winnerId, $looserId)
{
$connection = $this->getEntityManager()->getConnection();
$responseContent = [];
$tables = [
'StartupLifecycleEvent' => 'sp_startup_lifecycle_events',
'Application' => 'sp_applications',
];
foreach ($tables as $key => $name) {
$sql ="UPDATE {$name} s
SET s.startupId = ". $winnerId ."
WHERE s.startupId = " . $looserId;
$connection->executeQuery($sql);
$responseContent[] = "{$name} {$looserId} transfered to startup {$winnerId}";
$responseContent[] = "{$sql}";
}
$tables = [
'StartupAttribute' => 'sp_startup_attributes',
'StartupRelevance' => 'sp_startup_relevance',
];
foreach ($tables as $key => $name) {
$sql ="DELETE FROM {$name} WHERE startupId = " . $looserId;
$connection->executeQuery($sql);
$responseContent[] = "{$looserId} deleted from table {$name} ";
$responseContent[] = "{$sql}";
}
$sql = "DELETE FROM sp_startup_profiles WHERE id = :looserId";
$connection->executeQuery($sql, ['looserId' => $looserId]);
$responseContent[] = "{$looserId} deleted form table sp_startup_profiles";
$responseContent[] = "{$sql}";
return $responseContent;
}
public function saveStartup(Startup $startup, $adminEmail)
{
$em = $this->getEntityManager();
$now = new DateTime();
$startup->setLastModified($now);
$startup->setLastChangedBy($adminEmail);
$em->persist($startup);
$em->flush();
return $startup;
}
public function updateTeamName($teamId, $teamName)
{
$em = $this->getEntityManager();
if ($startup = $em->getRepository(Startup::class)->findOneBy(['teamId'=>$teamId])) {
$startup->setName($teamName);
$startup = $this->updateSlugByStartupName($startup);
$em->persist($startup);
$em->flush();
}
return true;
}
public function checkConnectionWithTeams(Startup $startup, $user)
{
$em = $this->getEntityManager();
$now = new DateTime();
$response = '';
if ($teamId = $startup->getTeamId()) {
if ($team = $em->getRepository(Team::class)->find($teamId)) {
if ($team->getStartupId() != $startup->getId()) {
$team->setStartupId($startup->getId());
$team->setLastModified($now);
$team->setLastChangeUser($user->getEmail());
$em->persist($team);
$em->flush();
$response = "Team updated with new startupId {$startup->getId()}";
}
} else {
$response = "ERROR team with {$startup->getTeamId()} does not exist";
}
}
return $response;
}
public function setStartupCompletenessScore(Startup $startup)
{
$credits = [
"oneSentencePitch" => 200,
"logoUrl" => 150,
"contactEmail" => 40,
"website" => 40,
"industry" => 20,
"foundationYear" => 20,
"customerFocus" => 20,
"stage" => 20,
"linkedin" => 30,
"twitter" => 15,
"instagram" => 15,
"xing" => 10,
"facebook" => 10,
];
$completenessScore = 0;
foreach ($credits as $field => $value) {
$method = 'get' . ucfirst($field);
if ($startup->$method()) {
$completenessScore += $value;
}
}
$tags = explode(',', str_replace('#', '', (string) $startup->getTags()));
if (in_array('accelerator', $tags)) {
$completenessScore += 50;
}
$tags = explode(',', str_replace('#', '', (string) $startup->getTags()));
if (in_array('gruenderstipendium', $tags)) {
$completenessScore += 30;
}
// UP-024: batch field removed from Startup - participation count could be used instead
// For now, removed from score calculation
if ($startup->getStage() == 'closed') {
$completenessScore = 0;
}
return $completenessScore;
}
public function updateScoreByEditTeam(Startup $startup)
{
$em = $this->getEntityManager();
$storedCompletenessScore = $startup->getScore();
$updatedCompletenessScore = $this->setStartupCompletenessScore($startup);
if ($startup->getScore() != $updatedCompletenessScore) {
$startup->setScore($updatedCompletenessScore);
}
return $startup;
}
public function getLatestEntriesByTeamTags($tag, $maxResults)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("t.tags LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$tag}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$startups = $qb->getQuery()->getResult();
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
't'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("t.tags LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$tag}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$teams = $qb->getQuery()->getResult();
$startupIds = array_map(fn (Team $entity) => $entity->getStartupId(), $teams);
$teams = array_combine($startupIds, $teams);
return ['startups' => $startups, 'teams' => $teams];
}
public function getLatestEntriesByTeamCategory($category, $maxResults)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("t.categories LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$category}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$startups = $qb->getQuery()->getResult();
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
't'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("t.categories LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$category}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$teams = $qb->getQuery()->getResult();
$startupIds = array_map(fn (Team $entity) => $entity->getStartupId(), $teams);
$teams = array_combine($startupIds, $teams);
return ['startups' => $startups, 'teams' => $teams];
}
public function getLatestEntriesByStartupTags($tag, $maxResults)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("s.tags LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$tag}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$startups = $qb->getQuery()->getResult();
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
't'
);
$qb->from(Startup::class, 's')
->from(Team::class, 't')
->where('s.teamId = t.id')
->andWhere("s.tags LIKE :tag")
->setMaxResults($maxResults)
->setParameter('tag', "%{$tag}%")
;
$qb->orderBy('s.lastModified', 'DESC');
$teams = $qb->getQuery()->getResult();
$startupIds = array_map(fn (Team $entity) => $entity->getStartupId(), $teams);
$teams = array_combine($startupIds, $teams);
return ['startups' => $startups, 'teams' => $teams];
}
public function removeStartup(Startup $startup)
{
$em = $this->getEntityManager();
$em->remove($startup);
$em->flush();
return "Startup removed";
}
public function setField($field, $action, $id, $adminEmail = "system@")
{
$em = $this->getEntityManager();
if (!$startup = $this->findOneBy(['id' => $id])) {
return 'ERROR: entity not found';
}
switch ($field) {
case "vorPitchOnly":
$startup->setPitchPosition('99');
$startup->setApplicationStatus('applied');
$startup->setBatchStatus('pre pitch');
break;
default:
$method = 'set' . ucfirst((string) $field);
$startup->$method($action);
break;
}
$startup->setLastModified(new DateTime());
$logText = "field {$field} set to {$action}";
$startup->setLastChangeUser($adminEmail);
$startup->setHistory(
"==== {$startup->getLastModified()->format('Y-m-d')} by {$startup->getLastChangeUser()}==="
. PHP_EOL . $logText . PHP_EOL .$startup->getHistory()
);
$em->persist($startup);
$em->flush();
return $logText;
}
public function setFieldByAjaxConnect($field, $action, Startup $startup, $user)
{
$em = $this->getEntityManager();
$adminEmail = $user->getEmail();
$method = 'set' . ($field);
$startup->$method($action);
$startup->setLastModified(new DateTime());
$startup->setLastChangeUser($user->getEmail());
$logText = "{$field}: $action";
$userType = $user->getIsAdmin() ? 'admin ' : 'member ';
$startup->setHistory(
"==== {$startup->getLastModified()->format('Y-m-d')} by {$userType} {$startup->getLastChangeUser()}==="
. PHP_EOL . $logText . PHP_EOL .$startup->getHistory()
);
$em->persist($startup);
$em->flush();
//sync with team
if($team = $em->getRepository(Team::class)->find($startup->getTeamId())) {
$em->getRepository(Team::class)->syncDataWithStartup($team, $startup);
}
switch ($field) {
case 'logoUrl':
$em->getRepository(Team::class)->unifyLogoUrlPartnerAndStartup($startup->getTeamId(), $action, $adminEmail);
$fieldName = 'logo';
break;
case 'imageLink':
$fieldName = 'banner';
break;
default:
$fieldName = $field;
}
return 'Updated ' . $fieldName;
}
public function find4Search($visibility)
{
$connection = $this->getEntityManager()->getConnection();
$andWhere = match ($visibility) {
"public" => " WHERE s.visibility = 'public'",
"sp-connect" => " WHERE s.visibility IN ('public','sp-connect')",
default => '',
};
$sql ="SELECT s.id, s.name, s.city, s.industry
FROM sp_startup_profiles s {$andWhere}
ORDER BY s.name ASC
";
return $connection->fetchAllAssociative($sql, []);
}
public function find4SearchWithBatch($batchIds)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('s.id, s.name, s.city, s.industry')
->from(Startup::class, 's')
->join(Application::class, 'a', 'WITH', 's.id = a.startupId')
->where('a.batchId in (:batchIds)')
->setParameter('batchIds', $batchIds)
;
return $qb->getQuery()->getResult();
}
public function findIndustries4Search($visibility)
{
$connection = $this->getEntityManager()->getConnection();
$andWhere = match ($visibility) {
"public" => " WHERE s.visibility = 'public'",
"sp-connect" => " WHERE s.visibility IN ('public','sp-connect')",
default => '',
};
$sql ="SELECT s.industry, count(s.id) as total
FROM sp_startup_profiles s {$andWhere}
GROUP BY s.industry
";
return $connection->fetchAllAssociative($sql, []);
}
public function find4Merge($startupId)
{
$connection = $this->getEntityManager()->getConnection();
$sql ="SELECT t.id, t.name, t.contactPerson, t.website, t.status, t.tags
FROM sp_startup_profiles t
WHERE t.id != :startupId
ORDER BY t.id DESC
";
return $connection->fetchAllAssociative($sql, ['startupId' => $startupId]);
}
public function findTags($criteria, $teamId)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's.tags'
);
$qb->from(Startup::class, 's')
->where("s.tags > '' ")
;
$qb = $this->setCriteria($qb, $criteria, $teamId);
$qb->orderBy('s.name', 'ASC');
$entities = $qb->getQuery()->getResult();
return Utility::countHashTags($entities, 'tags');
}
public function getListOfIndustries()
{
$em = $this->getEntityManager();
$attributes = $em->getRepository(Attribute::class)->findBy(['name'=>'startup.industry'], ['label'=>'ASC']);
$labels = array_map(fn ($entity) => $entity->getLabel(), $attributes);
$industries = array_combine($labels, $labels);
/*
$industries = $this->findIndustries([], 0);
return array_combine(array_keys($industries), array_keys($industries));
*/
return $industries;
}
public function findIndustries($criteria, $teamId)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
's.industry'
);
$qb->from(Startup::class, 's')
->where("s.industry > '' ")
;
$qb = $this->setCriteria($qb, $criteria, $teamId);
$qb->orderBy('s.name', 'ASC');
$entities = $qb->getQuery()->getResult();
return Utility::countHashTags($entities, 'industry');
}
public function findAndExpandAggregatesByField($field, $criteria, $teamId, $group='')
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
"s.{$field}"
);
$qb->from(Startup::class, 's')
->where("s.{$field} > '' ")
;
if ($group) {
$qb->andWhere("s.{$group} > '' ");
}
$qb = $this->setCriteria($qb, $criteria, $teamId);
$qb->orderBy('s.name', 'ASC');
$entities = $qb->getQuery()->getResult();
return Utility::countHashTags($entities, $field, true);
}
public function findApplicationsByProgramId($programId)
{
$connection = $this->getEntityManager()->getConnection();
$sqlRheinlandPitchApplicants = "
SELECT a.applicationStatus, a.batchStatus, count(a.id)
FROM sp_applications a, sp_batches b
WHERE a.batchId = b.id
AND a.programId = 1
GROUP BY a.applicationStatus, a.batchStatus;
Probleme macht der Batcht Status setBatchStatus(
gefunden haben wir
1_winner
2_podium
3_pitch
5_applied
6_started
vorgesehen sind:
applicant
pitch
pre pitch
SELECT b.id as batchId, b.batchNumber, b.name
FROM sp_applications a, sp_batches b
WHERE a.batchId = b.id
AND a.programId = 1 and a.batchStatus = 'pitch'
GROUP BY b.batchNumber
";
/*
*/
$sql ="
SELECT b.id as batchId, b.batchNumber, b.name, count(s.id) as number
FROM sp_startup_profiles s, sp_applications a, sp_batches b
WHERE s.id = a.startupId and a.batchId = b.id and a.programId = :programId and a.batchStatus = 'pitch'
GROUP BY b.batchNumber
";
$result = $connection->fetchAllAssociative($sql, ['programId'=>$programId]);
return $result;
}
public function findTeamTags($criteria, $teamId)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(
'a.teamTags'
);
$qb->from(StartupAttribute::class, 'a')
->where('a.teamId = :teamId')
->setParameter('teamId', $teamId)
;
$qb->innerJoin(Startup::class, 's', 'WITH', "a.startupId = s.id and a.teamTags > ''")
;
$qb = $this->setCriteria($qb, $criteria, $teamId);
$qb->orderBy('a.id', 'ASC');
$entities = $qb->getQuery()->getResult();
return Utility::countHashTags($entities, 'teamTags');
}
/**
* UP-024: Removed references to fields that no longer exist:
* - startplatz, accelerator, batch, batchFirst, batchLast, batchYear
* - rheinlandPitch, pitchNumber, pitchDate, pitchTopic
*/
public function getFieldsByTemplate($template)
{
$fields = match ($template) {
"rheinland-pitch" => ['id', 'name', 'website', 'todos', 'status', 'tags'],
"contact" => ['id', 'name', 'website', 'todos', 'status', 'city', 'location', 'contactPerson', 'contactEmail', 'contactTelefon'],
"startplatz" => ['id', 'name', 'website', 'todos', 'status', 'teamId', 'kiAccelerator'],
"description" => ['id', 'name', 'website', 'tags', 'oneSentencePitch'],
"kpi" => ['id', 'name', 'website', 'employeesCount', 'funding', 'investors', 'revenue', 'tags'],
"industries" => ['id', 'name', 'website', 'todos', 'industry', 'industryCondensed', 'customerFocus', 'businessModel'],
"logoAndTags" => ['id', 'name', 'website', 'domain', 'logoUrl', 'tags', 'todos'],
"full" => ['id', 'extId', 'name', 'teamId', 'website', 'domain', 'slug', 'status', 'kiAccelerator', 'todos', 'industry', 'industryCondensed', 'customerFocus', 'businessModel', 'tags', 'foundationYear', 'city', 'location', 'employeesCount', 'funding', 'investors', 'revenue', 'contactPerson', 'contactEmail', 'contactTelefon', 'oneSentencePitch'],
"default" => ['id', 'teamId', 'name', 'website', 'linkedin', 'contactPerson', 'contactEmail', 'contactTelefon', 'tags', 'oneSentencePitch'],
};
return $fields;
}
public function countByCriteria($criteria, $teamId)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('COUNT(s)')
->from(Startup::class, 's')
;
/*
->leftJoin(StartupAttribute::class, 'a', 'WITH', 's.id = a.startupId and a.teamId = :teamId')
->setParameter('teamId', $teamId)
*/
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria, $teamId);
}
return $qb->getQuery()->getSingleScalarResult();
}
public function findNotInRelevanceTable()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('s')
->from(Startup::class, 's')
->leftJoin(StartupRelevance::class, 'r', 'WITH', 's.id = r.startupId')
->where("r.startupId IS NULL")
;
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
public function findAcceleratorAlumnis()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('s')
->from(Startup::class, 's')
->innerJoin(Team::class, 't', 'WITH', 's.teamId = t.id')
->where("t.tags LIKE '%accelerator%'")
->andWhere("t.tags LIKE '%badge-accelerator%'")
->orderBy('s.name', 'ASC')
;
$results = $qb->getQuery()->getResult();
return $results;
}
/**
* Find all startups that are part of the KI Accelerator program
* (startups where kiAccelerator field is set)
*
* @return Startup[]
*/
public function findKiAcceleratorStartups(): array
{
return $this->createQueryBuilder('s')
->where('s.kiAccelerator IS NOT NULL')
->orderBy('s.kiAccelerator', 'ASC')
->addOrderBy('s.name', 'ASC')
->getQuery()
->getResult();
}
/**
* Find startups participating in the current Rheinland Pitch
*
* @param string $focusStatus The current focus status of the batch
* @return array Returns an array of Startup entities
*/
public function findCurrentRheinlandPitchStartups(string $focusStatus): array
{
return $this->createQueryBuilder('s')
// s represents startup entity
->select('s')
// Join with Application table connecting via startupId
->innerJoin(Application::class, 'a', 'WITH', 'a.startupId = s.id')
// Join with Batch table connecting via batchId
->innerJoin(Batch::class, 'b', 'WITH', 'a.batchId = b.id')
// Filter for Rheinland Pitch program (programId = 1)
->where('b.programId = :programId')
// Filter for current batch status
->andWhere('b.focusStatus = :focusStatus')
// Filter for pitch status in applications
->andWhere('a.batchStatus = :batchStatus')
->setParameter('programId', 1)
->setParameter('focusStatus', $focusStatus)
->setParameter('batchStatus', 'pitch')
// Order by pitch presentation order
->orderBy('a.pitchPosition', 'ASC')
->getQuery()
->getResult();
}
public function findPaginated($criteria, $teamId, $order = [], $page = 1, $limit = 5)
{
$page = (int) $page;
$limit = (int) $limit;
$qb = $this->getEntityManager()->createQueryBuilder();
$qb = $this->setSelect($qb);
$qb
->from(Startup::class, 's')
->innerJoin(StartupRelevance::class, 'r', 'WITH', 's.id = r.startupId')
->setFirstResult(max(0, $limit * ($page - 1))) // Offset
->setMaxResults($limit) // Limit
;
// get rid of
// ->leftJoin(StartupAttribute::class, 'a', 'WITH', 's.id = a.startupId and a.teamId = :teamId')
// ->setParameter('teamId', $teamId)
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria, $teamId);
}
if ($order) {
$metadata = $this->getClassMetadata();
foreach ($order as $key => $value) {
if ($key == 'score') {
$qb->addOrderBy('r.score', $value);
} elseif ($metadata->hasField($key)) {
$qb->addOrderBy('s.' . $key, $value);
}
// UP-024: Skip non-existent fields (removed: pitchDate, etc.)
}
} else {
$qb->addOrderBy('r.score', 'DESC');
$qb->addOrderBy('s.lastModified', 'DESC');
}
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
/**
* Paginated startup search with relevance sorting for SP-Connect.
* Relevance: logo → pitch → industry → relevance score → recency.
*/
public function findPaginatedWithSearch(array $criteria, $teamId, ?string $search, int $page = 1, int $limit = 72): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb = $this->setSelect($qb);
$qb->from(Startup::class, 's')
->innerJoin(StartupRelevance::class, 'r', 'WITH', 's.id = r.startupId')
->setFirstResult(max(0, $limit * ($page - 1)))
->setMaxResults($limit);
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria, $teamId);
}
if ($search) {
$qb->andWhere('(s.name LIKE :search OR s.industry LIKE :search OR s.oneSentencePitch LIKE :search)')
->setParameter('search', '%' . $search . '%');
// Search relevance: name match first, then industry, then pitch
$qb->addSelect("CASE WHEN s.name LIKE :search THEN 0 WHEN s.industry LIKE :search THEN 1 ELSE 2 END AS HIDDEN sortSearchRelevance")
->addOrderBy('sortSearchRelevance', 'ASC');
}
// Profile completeness sort: logo > pitch > score > recency
$qb->addSelect("CASE WHEN s.logoUrl IS NOT NULL AND s.logoUrl != '' THEN 0 ELSE 1 END AS HIDDEN sortLogo")
->addSelect("CASE WHEN s.oneSentencePitch IS NOT NULL AND s.oneSentencePitch != '' THEN 0 ELSE 1 END AS HIDDEN sortPitch")
->addOrderBy('sortLogo', 'ASC')
->addOrderBy('sortPitch', 'ASC')
->addOrderBy('r.score', 'DESC')
->addOrderBy('s.lastModified', 'DESC');
return $qb->getQuery()->getResult();
}
/**
* Count startups matching criteria + search for SP-Connect pagination.
*/
public function countWithSearch(array $criteria, $teamId, ?string $search): int
{
$qb = $this->getEntityManager()->createQueryBuilder()
->select('COUNT(s)')
->from(Startup::class, 's')
->innerJoin(StartupRelevance::class, 'r', 'WITH', 's.id = r.startupId');
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria, $teamId);
}
if ($search) {
$qb->andWhere('(s.name LIKE :search OR s.industry LIKE :search OR s.oneSentencePitch LIKE :search)')
->setParameter('search', '%' . $search . '%');
}
return (int) $qb->getQuery()->getSingleScalarResult();
}
/**
* UP-024: Removed fields that no longer exist on Startup entity:
* - startplatz, accelerator, batch, batchLast, batchFirst, batchYear
* - rheinlandPitch, pitchNumber, pitchDate, pitchTopic
* - gruenderstipendium, gsJury, gsStatus
* - hasBadgeStartplatz
* These are now managed via Participation entity.
*/
private function setSelect($qb)
{
return $qb
->select('s.id', 's.extId', 's.visibility', 's.teamId', 's.name', 's.status', 's.location', 's.city'
, 's.website', 's.domain', 's.slug', 's.linkedin', 's.twitter', 's.logoUrl', 's.stage'
, 's.kiAccelerator'
, 's.industry', 's.industryCondensed', 's.customerFocus', 's.businessModel', 's.tags', 's.todos'
, 's.foundationYear', 's.funding', 's.oneSentencePitch'
, 's.employeesCount', 's.revenue', 's.investors'
, 's.contactPerson', 's.contactEmail', 's.contactTelefon', 's.lastModified', 's.history', 's.lastChangeUser', 's.createdAt'
, 'r.score'
)
;
}
public function findNotPaginated($criteria, $teamId, $order)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb = $this->setSelect($qb);
$qb
->from(Startup::class, 's')
->innerJoin(StartupRelevance::class, 'r', 'WITH', 's.id = r.startupId')
->leftJoin(StartupAttribute::class, 'a', 'WITH', 's.id = a.startupId and a.teamId = :teamId')
->setParameter('teamId', $teamId)
;
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria, $teamId);
}
if ($order) {
$metadata = $this->getClassMetadata();
foreach ($order as $key => $value) {
if ($metadata->hasField($key)) {
$qb->addOrderBy('s.' . $key, $value);
}
// UP-024: Skip non-existent fields (removed: pitchDate, etc.)
}
} else {
$qb->addOrderBy('r.score', 'DESC');
}
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
public function findSingleStartupByVisibility($id, $visibility)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('s')
->from(Startup::class, 's')
->where('s.id = :id')
->setParameter('id', $id)
;
switch ($visibility) {
case "public":
$qb->andWhere("s.visibility = 'public' ");
break;
case "sp-connect":
$qb->andWhere("s.visibility IN ('public','sp-connect')");
break;
default:
break;
}
return $qb->getQuery()->getOneOrNullResult();
}
/*
if ($criteria){
foreach($criteria as $field =>$value) {
if ($field == 'categories' or $field == 'tags'){
$qb->andWhere('t.'. $field . " LIKE '%" . $value . "%'");
} elseif ($value == 'IS NULL' or $value == null) {
$qb->andWhere('t.'. $field . " IS NULL" );
} elseif (is_array($value) ) {
$qb->andWhere('t.'. $field . " IN (:{$field})" );
$qb->setParameter($field, array_keys($value));
}
else {
$qb->andWhere('t.'. $field . ' = :' . $field);
$qb->setParameter($field, $value);
}
}
}
*/
private function setCriteria(QueryBuilder $qb, $criteria, $teamId = 0)
{
foreach($criteria as $field =>$value) {
switch ($field) {
case "teamTags":
$qb->innerJoin(StartupAttribute::class, 'b', 'WITH', 's.id = b.startupId and b.teamId = :teamIdInnerJoin');
$qb->andWhere('b.'. $field . ' LIKE :' . $field);
$qb->setParameter($field, "%{$value}%");
$qb->setParameter('teamIdInnerJoin', $teamId);
break;
case "batchNumber":
$qb->innerJoin(Application::class, 'ap', 'WITH', 's.id = ap.startupId and ap.programId = 1');
$qb->innerJoin(Batch::class, 'batch', 'WITH', 'ap.batchId = batch.id');
$qb->andWhere('batch.'. $field . ' LIKE :' . $field);
$qb->setParameter($field, "%{$value}%");
break;
// UP-024: gsStatus field removed from Startup - filter via Participation instead
case "visibility":
switch ($value) {
case "public":
$qb->andWhere('s.'. $field . ' = :' . $field);
$qb->setParameter($field, $value);
break;
case "sp-connect":
$qb->andWhere("s.visibility IN ('public','sp-connect')");
break;
default:
break;
}
break;
case "team":
$qb->andWhere('s.teamId > 0');
break;
case "todos":
case "tags":
// UP-024: pitchNumber removed - field no longer exists on Startup entity
$qb->andWhere('s.'. $field . ' LIKE :' . $field);
$qb->setParameter($field, "%{$value}%");
break;
case "isBookmarked":
$qb->innerJoin(StartupAttribute::class, 'b', 'WITH', 's.id = b.startupId and b.teamId = :teamIdInnerJoin');
$qb->andWhere('b.'. $field . ' = 1');
$qb->setParameter('teamIdInnerJoin', $teamId);
break;
default:
// Skip unknown fields to prevent QueryException for removed fields (e.g., startplatz)
$metadata = $this->getClassMetadata();
if (!$metadata->hasField($field) && !$metadata->hasAssociation($field)) {
continue 2; // continue the foreach loop
}
if ($value == 'IS NULL' or $value == null) {
$qb->andWhere('s.'. $field . " IS NULL");
} elseif ($value == 'not empty') {
$qb->andWhere('s.'. $field . " > '' ");
} elseif (is_array($value)) {
$qb->andWhere('s.'. $field . " IN (:{$field})");
$qb->setParameter($field, array_keys($value));
} else {
$qb->andWhere('s.'. $field . ' = :' . $field);
$qb->setParameter($field, $value);
}
break;
}
}
return $qb;
}
private function expandTimeCriteria($qb, $value)
{
$startDate = date_create($value);
$endDate = date_create($value);
$endDate->modify('+1 days');
$qb->andWhere('c.start BETWEEN :startDate AND :endDate')
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
;
return $qb;
}
public function importOrUpdateByApplication(Application $application, User $user, $importTag)
{
$em = $this->getEntityManager();
$now = new DateTime();
$adminEmail = $user->getEmail();
$status = 'open';
$todo = '';
$responseContent = [];
$exportList = [];
$preFields = [];
$responseText = "Case #{$application->getStartupName()} ";
$program = $em->getRepository(Program::class /*Startup Program*/)->findOneBy(['id'=>$application->getProgramId()]);
$batch = $em->getRepository(Batch::class)->findOneBy(['id'=>$application->getBatchId()]);
if ($application->getWebsiteUrl()) {
$domain = $this->extractDomainByWebsite($application->getWebsiteUrl());
if (in_array($domain, $this->getDomainServices())) {
$domain = $application->getWebsiteUrl() ;
}
} else {
$domain = $this->extractDomainByEmail($application->getEmail());
if (in_array($domain, $this->getMailServices())) {
$domain = str_replace('@', 'at', $application->getEmail()) ;
}
}
// check if application->teamId is virtual team
if ($teamId = $application->getTeamId()) {
if (!$team = $em->getRepository(Team::class)->find($teamId)) {
$teamId = "";
} else {
if ($team->getType() == 'Virtual Team') {
$teamId = "";
}
}
}
// check if startup is already imported
if (!$startup = $this->findOneBy(['id' => $application->getStartupId()])) {
// check if there is already a startup with the application teamId
if (!$startup = $this->findOneBy(['teamId'=>$application->getTeamId()])) {
// lookup domain
if ($startup = $this->findOneBy(['domain'=>$domain])) {
$response = [
'status' => "ERROR",
'todo' => "checkDomain",
'message' => "Domain {$domain} is already stored with startup {$startup->getName()} [{$startup->getId()}] ",
'data' => $startup,
];
return $response;
} elseif ($startup = $this->findOneBy(['teamId' => $team->getId()])) {
$response = [
'status' => "ERROR",
'todo' => "checkTeamId",
'message' => "teamId {$teamId} is already stored with startup {$startup->getName()} [{$startup->getId()}] ",
'data' => $startup,
];
return $response;
} else {
$startup = new Startup();
$startup->setTeamId($teamId);
$startup->setDomain($domain);
// UP-024: setStartplatz removed - no longer tracked on Startup
}
}
}
if ($startup->getId()) {
$responseText .= "updated on ";
} else {
$responseText .= "imported ";
}
$startup->setName($application->getStartupName());
if ($startup->getName() and !$startup->getSlug()) {
$startup = $this->updateSlugByStartupName($startup);
}
$startup->setStatus('active');
// UP-024: Rheinland-Pitch data now managed via Participation entity
// setOrUpdateRheinlandPitchSpecials and pitchNumber/pitchDate/pitchTopic removed
// Use ParticipationOutcomeWriter::recordRheinlandPitchOutcome() for outcomes
$startup->setWebsite($this->sanitizeWebsite($application->getWebsiteUrl()));
$startup->setCity($application->getCity());
$startup->setEmployeesCount($application->getTeamSize());
$startup->setStage($application->getStartupStage());
$startup->setContactPerson($application->getPerson());
$startup->setContactEmail($application->getEmail());
$startup->setContactTelefon($this->sanitizePhone($application->getPhone()));
$startup->setLinkedin($application->getLinkedin());
$startup->setOneSentencePitch($this->sanitizeDescription($application->getIdeaPitch()));
$startup->setVisibility($this->setVisibilityByApplicationProgramStatus($application, $startup->getVisibility()));
$startup->setLastChangedBy($adminEmail);
$em->persist($startup);
$em->flush();
$responseText .= "application has been imported";
if (!$startupAttribute = $em->getRepository(StartupAttribute::class)->findOneBy(['startupId'=> $startup->getId(), 'teamId'=>$user->getMemberId()])) {
$startupAttribute = new StartupAttribute();
$startupAttribute->setTeamId($user->getMemberId());
$startupAttribute->setStartupId($startup->getId());
}
$startupAttribute->setTeamTags($em->getRepository(Tag::class)->addHashTag($importTag, $startupAttribute->getTeamTags()));
$startupAttribute->setLastModified($now);
$startupAttribute->setLastChangedBy($adminEmail);
$em->persist($startupAttribute);
$em->flush();
$em->getRepository(StartupRelevance::class)->updateByUserAction($startup->getId(), 'create');
if (!$application->getStartupId()) {
$application->setStartupId($startup->getId());
$application->setEditStatus('imported in startup table');
$responseText .= " startupId has been set in application";
} else {
$application->setEditStatus('updated in startup table');
$responseText .= " updated in startup table";
}
$em->persist($application);
$em->flush();
// check if startupId need to be set in table team
if ($startup->getTeamId() > 0) {
$em->getRepository(Team::class)->setStartupIdIfNotExists($startup->getTeamId(), $startup->getId(), $adminEmail);
}
$responseContent[] = $responseText . " done";
$response = [
'status' => "SUCCESS",
'todo' => "done",
'message' => $responseContent,
'data' => $startup,
];
return $response;
}
/**
* @deprecated UP-024: Removed - Rheinland-Pitch data now managed via Participation entity
* Use ParticipationOutcomeWriter::recordRheinlandPitchOutcome() instead
*/
public function setOrUpdateRheinlandPitchSpecials(Startup $startup, Application $application)
{
// UP-024: This method is now a no-op
// Rheinland-Pitch participation and outcomes are stored in sp_ecosystem_participation
// See ParticipationOutcomeWriter::recordRheinlandPitchOutcome()
return $startup;
}
private function setVisibilityByApplicationProgramStatus(Application $application, $currentVisibility)
{
switch ($application->getBatchStatus()) {
case "pitch":
return "public";
case "pre pitch":
if ($currentVisibility != "public") {
return "sp-connect";
} else {
return $currentVisibility;
}
// no break
case "applicant":
if (!$currentVisibility) {
return "admin";
} else {
return $currentVisibility;
}
}
}
public function importOrUpdateByTeam(Team $team, $user = false, $importTag = false)
{
$em = $this->getEntityManager();
$now = new DateTime();
if ($user) {
$adminEmail = $user->getEmail();
} else {
$adminEmail = "system@";
}
if (!$team->getCategories()){
$team = $em->getRepository(Team::class)->setCategory($team, "startup", $adminEmail);
}
$todos = [];
$responseText = "Case #{$team->getName()} ";
$teamCategories = explode(',', str_replace(' ', '', $team->getCategories() ?? ''));
$teamTags = array_filter(explode('#', str_replace(' ', '', $team->getTags() ?? '')));
// check if startup is already imported
if (!$startup = $this->findOneBy(['teamId' => $team->getId()])) {
$startup = new Startup();
$startup->setTeamId($team->getId());
$startup->setStatus('active');
$startup->setCreatedAt($now);
}
$tags = $startup->getTags();
if (!$startup->getWebsite()) {
if (!$team->getHomepage()) {
$todos[] = "check homepage";
} else {
$domain = $this->cleanDomain($team->getHomepage());
$startup->setDomain($domain);
$startup->setWebsite($this->sanitizeWebsite($team->getHomepage()));
}
}
if ($startup->getId()) {
$responseText .= "updated on ";
} else {
$responseText .= "imported ";
}
if (!$startup->getName()) {
$startup->setName($team->getName());
if ($startup->getName() and !$startup->getSlug()) {
$startup = $this->updateSlugByStartupName($startup);
}
}
// UP-024: Program-specific fields removed from Startup
// startplatz, gruenderstipendium, gsJury, gsStatus, accelerator, batch, batchYear
// are now managed via Participation entity
if ($team->getGsStatus()) {
$tags = $em->getRepository(Tag::class)->addHashTag('gruenderstipendium', $tags);
$tags = $em->getRepository(Tag::class)->addHashTag('sp-connect', $tags);
}
if (in_array('accelerator', $teamCategories)) {
$tags = $em->getRepository(Tag::class)->addHashTag('accelerator', $tags);
$tags = $em->getRepository(Tag::class)->addHashTag('sp-connect', $tags);
}
if (!$startup->getCity()) {
$startup->setCity($team->getAddressCity());
}
if (!$startup->getEmployeesCount()) {
$startup->setEmployeesCount($team->getNEmployees());
}
if (!$startup->getContactPerson()) {
$startup->setContactPerson($team->getPerson());
}
if(!$startup->getLogoUrl() and $team->getLogoUrl()) {
$startup->setLogoUrl($team->getLogoURL());
}
if (!$startup->getContactEmail()) {
$startup->setContactEmail($team->getConnectEmail());
}
if (!$startup->getContactTelefon()) {
$startup->setContactTelefon($team->getPhone());
}
if (!$startup->getStage()) {
$startup->setStage('idea');
}
if (!$startup->getCustomerFocus()) {
$startup->setCustomerFocus('B2B');
}
if (!$startup->getIndustry()) {
$startup->setIndustry($team->getIndustries());
}
$startup->setTags($tags);
if (!$startup->getOneSentencePitch()) {
$startup->setOneSentencePitch($this->sanitizeDescription($team->getDescription()));
}
$startup->setLastChangedBy($adminEmail);
$startup->setTodos(implode(',', $todos));
$startup->setLastModified($now);
$em->persist($startup);
$em->flush();
$responseText .= "application has been imported";
if ($user and $importTag) {
if (!$startupAttribute = $em->getRepository(StartupAttribute::class)->findOneBy(['startupId'=> $startup->getId(), 'teamId'=>$user->getMemberId()])) {
$startupAttribute = new StartupAttribute();
$startupAttribute->setTeamId($user->getMemberId());
$startupAttribute->setStartupId($startup->getId());
}
$startupAttribute->setTeamTags($em->getRepository(Tag::class)->addHashTag($importTag, $startupAttribute->getTeamTags()));
$startupAttribute->setLastModified($now);
$startupAttribute->setLastChangedBy($adminEmail);
$em->persist($startupAttribute);
$em->flush();
$em->getRepository(StartupRelevance::class)->updateByUserAction($startup->getId(), 'create');
}
if (!$team->getStartupId()) {
$team = $em->getRepository(Team::class)->setStartupId($team, $startup->getId(), $adminEmail);
$responseText .= " startupId has been set in team";
}
$responseText .= " done";
return $responseText;
}
public function updateWebsiteByTeamHomepage(Startup $startup, $homepage)
{
$domain = $this->cleanDomain($homepage);
$startup->setDomain($domain);
$startup->setWebsite($this->sanitizeWebsite($homepage));
return $startup;
}
public function syncDataWithTeam(Startup $startup, Team $team)
{
$update = false;
if ($team->getShortName() and $startup->getName() != $team->getShortName()) {
$startup->setName($team->getShortName());
$update = true;
}
if ($team->getLogoURL() and $startup->getLogoUrl() != $team->getLogoURL()) {
$startup->setLogoUrl($team->getLogoURL());
$update = true;
}
if ($team->getHomepage() and $startup->getWebsite() != $team->getHomepage()) {
$startup = $this->updateWebsiteByTeamHomepage($startup, $team->getHomepage());
$update = true;
}
if ($team->getLinkedin() and $startup->getLinkedin() != $team->getLinkedin()) {
$startup->setLinkedin($team->getLinkedin());
$update = true;
}
if ($team->getXing() and $startup->getXing() != $team->getXing()) {
$startup->setXing($team->getXing());
$update = true;
}
if ($team->getTwitter() and $startup->getTwitter() != $team->getTwitter()) {
$startup->setTwitter($team->getTwitter());
$update = true;
}
if ($team->getFacebook() and $startup->getFacebook() != $team->getFacebook()) {
$startup->setFacebook($team->getFacebook());
$update = true;
}
if ($team->getInstagram() and $startup->getInstagram() != $team->getInstagram()) {
$startup->setInstagram($team->getInstagram());
$update = true;
}
if ($update) {
$startup->setScore($this->setStartupCompletenessScore($startup));
$em = $this->getEntityManager();
$em->persist($startup);
$em->flush();
}
return $startup;
}
public function updateSlugByStartupName(Startup $startup)
{
$slug = Utility::generateSlug($startup->getName());
if ($startup->getSlug() != $slug) {
$connection = $this->getEntityManager()->getConnection();
$oldSlug = $startup->getSlug();
if ($oldSlug != null) {
// replace old slug with new slug
// in memberPosts
$sql = "
UPDATE sp_content_posts
SET tags = REPLACE(tags,:oldSlug,:slug)
WHERE tags LIKE :queryString
";
$connection->executeQuery($sql, ['oldSlug'=>$oldSlug, 'slug'=>$slug, 'queryString' => '%'.$oldSlug.'%']);
// in feeds
$sql = "
UPDATE sp_content_feed
SET tags = REPLACE(tags,:oldSlug,:slug)
WHERE tags LIKE :queryString
";
$connection->executeQuery($sql, ['oldSlug'=>$oldSlug, 'slug'=>$slug, 'queryString' => '%"'.$oldSlug.'"%']);
}
$startup->setSlug($slug);
}
return $startup;
}
public function updateByEdit(Startup $startup, $differences, $adminEmail)
{
$em = $this->getEntityManager();
if ($differences) {
foreach ($differences as $diff => $value) {
switch ($diff) {
case "id":
break;
case "website":
$startup->setWebsite(rtrim((string) $value, "/"));
$domain = $this->cleanDomain($startup->getWebsite());
if ($startup->getDomain() != $domain) {
$startup->setDomain($domain);
}
break;
default:
$method = 'set' . ucfirst((string) $diff);
$startup->$method($value);
break;
}
}
$startup->setScore($this->setStartupCompletenessScore($startup));
$startup->setLastChangedBy($adminEmail);
$em->persist($startup);
$em->flush();
}
return $startup;
}
public function setComments(Startup $startup, $addNote, $adminEmail)
{
$em = $this->getEntityManager();
$startup->setLastChangeUser($adminEmail);
$startup->setComments('==== '. date("Y-m-d") . ' by ' . $startup->getLastChangeUser() .' ==='. PHP_EOL . $addNote . PHP_EOL . $startup->getComments());
$logText = "added Comment";
$startup->setHistory('==== '. date("Y-m-d") . ' by ' . $startup->getLastChangeUser() .' ==='. PHP_EOL . $logText . PHP_EOL . $startup->getHistory());
$em->persist($startup);
$em->flush();
return $startup;
}
public function importOrUpdate($feedback, $fields, User $user, $template, $importTag)
{
$em = $this->getEntityManager();
$now = new DateTime();
$adminEmail = $user->getEmail();
$responseContent = [];
$exportList = [];
$preFields = [];
$j = 0;
foreach($feedback as $row) {
$j++;
$responseText = "Case #{$j} ";
// check if array in row contains as many elements as fields
if ((is_countable($fields) ? count($fields) : 0) != (is_countable($row) ? count($row) : 0)) {
$i = 0;
foreach ($fields as $field) {
$newRow[ $field ] = $row[$i] ?? '' ;
$i++;
}
$row = $newRow;
} else {
$row = array_combine($fields, $row);
}
if (array_key_exists('action', $row) and $row['action'] == 'checkUrl') {
$preData = [
'sp_id' => '99999',
'sp_teamId' => '77777',
'sp_name' => 'n.a.',
'sp_website' => 'n.a.',
];
$preFields = array_keys($preData);
if (!$row['website']) {
$exportList[] = implode("\t", array_merge($preData, $row));
continue;
}
$domain = $this->cleanDomain($row['website']);
if ($startup = $this->findOneByLike('domain', $domain)) {
$preData = [
'sp_id' => $startup->getId(),
'sp_teamId' => $startup->getTeamId(),
'sp_name' => $startup->getName(),
'sp_website' => $startup->getWebsite(),
];
}
$exportList[] = implode("\t", array_merge($preData, $row));
continue;
}
if (array_key_exists('action', $row)) {
$stopProcessing = false;
switch ($row['action']) {
case "addApplication":
$responseContent[] = $this->importAddApplication($row, $fields, $adminEmail);
$stopProcessing = true;
break;
case "delete":
$em->remove($startup);
$em->flush();
$responseContent[] = $responseText . " entry permanently removed";
$stopProcessing = true;
break;
}
if ($stopProcessing) {
continue; // go to next iteration in loop
}
}
if (!$startup = $this->findOneBy(['id'=>$row['id']])) {
// check if startup is already imported
if (!$startup = $this->findAlreadyImportedstartup($row, $template)) {
$startup = new Startup();
}
} else {
$responseText .= "{$startup->getId()} ";
}
if (in_array('phone', array_keys($row)) or in_array('website', array_keys($row)) or in_array('segment', array_keys($row))) {
$row = $this->sanitizeRowData($row);
}
// get rid of elements with empty value
// $row = array_filter($row);
$startupData = $this->fillEntityData($startup, $fields);
if ($startup->getImportNote() == 'foundByDomain') {
if (array_key_exists('overwrite', $row) and $row['overwrite'] == 'yes') {
$responseText .= "content will be overwritten ";
} else {
$exportList[] = implode("\t", $startupData + ['overwrite' => 'no']) ;
$exportList[] = implode("\t", $this->fillRow($row, $fields) + ['overwrite' => 'no']);
$responseContent[] = $responseText . "no import - foundByDomain";
continue;
}
$startup->setImportNote('');
}
$differences = array_diff($row, $startupData);
$differences2 = array_diff($startupData, $row);
$differences = $differences + $differences2;
if ($differences) {
if ($startup->getId()) {
$responseText .= "updated on ";
} else {
$responseText .= "imported ";
}
foreach ($differences as $diff => $value) {
switch ($diff) {
case "id":
break;
case "action":
if ($row['action'] == 'checkLogo') {
$startup = $this->checkLogo($startup);
if ($startup->getImportNote() and $startup->getImportNote() != "Bad Request") {
$suggestions = json_decode((string) $startup->getImportNote());
$exportParts = '';
$exportPartsArray = [];
foreach ($suggestions as $suggestion) {
$columns = array_values((array) $suggestion);
$exportPartsArray = [...$exportPartsArray, ...$columns];
}
$exportList[] = implode("\t", $this->fillRow($row, $fields) + $exportPartsArray);
$startup->setImportNote('');
}
}
break;
case "website":
$startup->setWebsite(rtrim((string) $row['website'], "/"));
$domain = $this->cleanDomain($startup->getWebsite());
if ($startup->getDomain() != $domain) {
$startup->setDomain($domain);
}
break;
case "domain":
if (!$domain = $row['domain']) {
if (array_key_exists('website', $row)) {
$domain = $this->cleanDomain($row['website']);
}
}
$startup->setDomain($domain);
break;
case "pitchNumber":
// UP-024: pitchNumber field removed - creating application only
$program = $em->getRepository(Program::class /*Startup Program*/)->findOneBy(['id'=>1]);
$em->persist($startup);
$em->flush();
$startupArray['id'] = $startup->getId();
$responseText .= $em->getRepository(Application::class)->createApplicationByStartup($startupArray, $program, $adminEmail);
break;
case "todos":
$todosArrayStored = [];
if ($startup->getTodos()) {
$todosArrayStored = json_decode((string) $startup->getTodos(), true);
}
$todosArrayImported = explode(',', str_replace(', ', ',', (string) $row['todos']));
$todosArray = array_merge($todosArrayImported, $todosArrayStored);
array_filter($todosArray);
$todosArray = array_unique($todosArray);
$todosJson = json_encode(array_values($todosArray));
$startup->setTodos($todosJson);
break;
case "tags":
$startup->setTags($em->getRepository(Tag::class)->updateHashTags($row['tags'], $startup->getTags()));
break;
case "status":
$startup->setStatus($row['status'] ?: 'unknown');
break;
case "startplatz":
case "batch":
case "batchLast":
case "pitchDate":
// UP-024: These fields removed from Startup - managed via Participation entity
break;
case "city":
if ($row['city'] == 'CGN' or $row['city'] == 'DUS') {
$startup->setLocation($row['city']);
if ($row['city'] == 'CGN') {
$startup->setCity('Köln');
}
if ($row['city'] == 'DUS') {
$startup->setCity('Düsseldorf');
}
} else {
$startup->setCity($row['city']);
}
break;
default:
$method = 'set' . ucfirst($diff);
$startup->$method(trim((string) $row[$diff]));
break;
}
$responseText .= " {$diff} with {$row[$diff]}, ";
}
if ($startup->getWebsite() and !$startup->getDomain()) {
$startup->setDomain($this->cleanDomain($startup->getWebsite()));
}
if ($startup->getName() and !$startup->getSlug()) {
$startup = $this->updateSlugByStartupName($startup);
}
if (!$startup->getStatus()) {
$startup->setStatus('unknown');
}
// UP-024: startplatz, accelerator, rheinlandPitch fields removed
// These are now managed via Participation entity
if (!$startup->getIndustry()) {
$startup->setIndustry('Other');
}
if (!$startup->getIndustryCondensed()) {
$startup->setIndustryCondensed('Other');
}
if (!$startup->getCustomerFocus()) {
$startup->setCustomerFocus('unknown');
}
if (!$startup->getBusinessModel()) {
$startup->setBusinessModel('Other');
}
$startup->setLastChangedBy($adminEmail);
$em->persist($startup);
$em->flush();
// check if teamId
if ($startup->getTeamId()>0) {
$em->getRepository(Team::class)->setStartupIdIfNotExists($startup->getTeamId(), $startup->getId(), $adminEmail);
}
if (!$startupAttribute = $em->getRepository(StartupAttribute::class)->findOneBy(['startupId'=> $startup->getId(), 'teamId'=>$user->getMemberId()])) {
$startupAttribute = new StartupAttribute();
$startupAttribute->setTeamId($user->getMemberId());
$startupAttribute->setStartupId($startup->getId());
}
$em->getRepository(StartupRelevance::class)->updateByUserAction($startup->getId(), 'create');
$startupAttribute->setTeamTags($em->getRepository(Tag::class)->addHashTag($importTag, $startupAttribute->getTeamTags()));
$startupAttribute->setLastModified($now);
$startupAttribute->setLastChangedBy($adminEmail);
$em->persist($startupAttribute);
$em->flush();
$responseContent[] = $responseText . "row has been imported";
} else {
$responseContent[] = $responseText . "no update";
}
//$responseContent[] = $startupData;
}
return ['responseContent' =>$responseContent, 'exportList' => $exportList, 'preFields' => $preFields];
}
private function importAddApplication($row, $fields, $adminEmail)
{
$em = $this->getEntityManager();
$now = new \DateTime();
$domain = $this->extractDomainByWebsite($row['website']);
$startup = $this->findOrCreateStartupByDomain($domain);
$batch = $em->getRepository(Batch::class)->findOneBy(['programId'=>$row['programId'], 'batchNumber'=>$row['batchNumber']]);
/** @var Program $program */
$program = $batch->getProgram();
$application = $em->getRepository(Application::class)->findOrCreateApplicationByWebsiteUrl($row['website']);
if (!$startup->getId()) {
$startup->setName($row['name']);
$startup->setWebsite($row['website']);
$startup->setOneSentencePitch($row['oneSentencePitch']);
$startup->setContactEmail($row['email']);
$startup->setLastChangedBy($adminEmail);
$startup->setCreatedAt($now);
$startup->setLastModified($now);
$em->persist($startup);
$em->flush();
}
if (!$teamId = $startup->getTeamId()) {
// find virtual team of that batch
$settings = (array) json_decode((string) $batch->getSettings(), true);
$team = $em->getRepository(Team::class)->findOneBy(['shortName'=>$settings['virtualTeam']]);
$teamId = $team->getId();
}
$application->setStartupId($startup->getId());
$application->setTeamId($teamId);
$application->setEmail($row['email']);
$application->setStartupName($row['name']);
$application->setProgramId($row['programId']);
$application->setBatchId($batch->getId());
$application->setBatchName($batch->getName());
$application->setBatchSlug($batch->getSlug());
$application->setTemplate($batch->getTemplate());
$application->setProgramSlug($program->getSlug());
$application->setProgramName($program->getName());
$application->setApplicationStatus('approved');
$application->setBatchStatus($row['status']);
$application->setIdeaPitch($row['oneSentencePitch']);
$application->setLastChangeUser($adminEmail);
$application->setCreatedAt($now);
$application->setLastModified($now);
$em->persist($application);
$em->flush();
return "application created with id={$application->getId()}";
}
private function findOrCreateStartupByDomain($domain)
{
$em = $this->getEntityManager();
if (!$startup = $this->findOneBy(['domain'=>$domain])) {
$startup = new Startup();
$startup->setDomain($domain);
}
return $startup;
}
public function setEmptyStartup()
{
return new Startup();
}
private function findOneByLike($field, $criterium)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('c')
->from(Startup::class, 'c')
->where("c.{$field} LIKE :{$field}")
->setParameter($field, "%{$criterium}%")
->setMaxResults(1)
->orderBy('c.lastModified', 'DESC')
;
return $qb->getQuery()->getOneOrNullResult();
}
private function fillRow($row, $fields)
{
foreach ($fields as $field) {
$oldData[$field] = array_key_exists($field, $row) ? $row[$field] : '' ;
}
return $oldData;
}
private function fillEntityData($entity, $fields)
{
foreach ($fields as $field) {
$method = 'get' . ucfirst((string) $field);
if ($entity->$method() instanceof DateTime) {
$oldData[$field] = $entity->$method()->format('Y-m-d') ;
} elseif (in_array($field, ['todos'])) {
if ($entity->$method()) {
$oldData[$field] = implode(',', json_decode((string) $entity->$method(), true));
} else {
$oldData[$field] = '';
}
} else {
$oldData[$field] = trim((string) $entity->$method());
}
// UP-024: Removed 'startplatz' and 'accelerator' - fields no longer exist
if (in_array($field, ['status']) and !$oldData[$field]) {
$oldData[$field] = 'default';
}
}
return $oldData;
}
private function sanitizeRowData($row)
{
if (in_array('phone', array_keys($row))) {
$row['phone'] = $this->sanitizePhone($row['phone']);
}
if (in_array('website', array_keys($row)) and $row['website']) {
$row['website'] = $this->sanitizeWebsite($row['website']);
}
if (in_array('todos', array_keys($row)) and $row['todos']) {
$row['todos'] = str_replace(', ', ',', (string) $row['todos']);
}
if (in_array('segment', array_keys($row))) {
$row['segment'] = $this->sanitizeSegment($row['segment']);
}
return $row;
}
private function sanitizeWebsite($website)
{
$website = strtolower((string) $website);
if (!(str_starts_with($website, 'https://') or str_starts_with($website, 'http://'))) {
$website = 'https://' . $website;
}
$website = str_replace('http://', 'https://', $website);
return $website;
}
private function sanitizePhone($phone)
{
$phone = str_replace('+49 (0) ', '+49-', (string) $phone);
$phone = str_replace('+49 ', '+49-', $phone);
$phone = str_replace('/', '-', $phone);
$phone = str_replace('(', '', $phone);
$phone = str_replace(') ', '-', $phone);
$phone = str_replace(' ', '', $phone);
if (substr((string) $phone, 0, 1) === 0) {
$phone = '+49-' . substr((string) $phone, 1);
}
if (str_starts_with($phone, '49')) {
$phone = '+' . $phone;
}
if (substr((string) $phone, 3, 1) !== '-') {
$phone = substr((string) $phone, 0, 3) . '-' . substr((string) $phone, 3);
}
return $phone;
}
private function sanitizeDescription($description)
{
$string = str_replace("\r\n", "<br>", (string) $description);
$string = str_replace("\r", "", $string);
$string = str_replace("\t", "", $string);
$string = str_replace("\n", "", $string);
$string = str_replace("<p>", "", $string);
$string = str_replace("</p>", "", $string);
return $string;
}
private function findAlreadyImportedStartup($row, $template)
{
$startup = null;
switch ($template) {
case "ddw":
if (!$startup = $this->findOneBy(['ddwId'=>$row['ddwId']])) {
if ($startup = $this->findByWebsite($row['website'])) {
$startup->setImportNote('foundByWebsite');
}
}
break;
case "bizcover":
if (!$startup = $this->findByRegister($row['registerGericht'], $row['registerNumber'])) {
$startup = $this->findByWebsite($row['website']);
if ($startup and $startup->getRegisterGericht()) {
$startup = null;
} else {
$startup->setImportNote('foundByWebsite');
}
}
break;
case "default":
if (array_key_exists('extId', $row)) {
if ($startup = $this->findOneBy(['extId'=>$row['extId']])) {
$startup->setImportNote('foundByExtId');
break;
}
}
// check if domain is set
if (array_key_exists('domain', $row)) {
if (!$domain = $row['domain']) {
if (array_key_exists('website', $row) and $row['website']) {
$domain = $this->cleanDomain($row['website']);
}
}
if ($startup = $this->findOneBy(['domain'=>$domain])) {
$startup->setImportNote('foundByDomain');
}
}
break;
}
return $startup;
}
private function setSlug($name)
{
return Utility::generateSlug($name);
}
private function cleanDomain($website)
{
$website = str_replace("https://www.", "", (string) $website);
$website = str_replace("http://www.", "", $website);
$website = str_replace("https://", "", $website);
$website = str_replace("http://", "", $website);
$website = explode('/', $website)[0];
return $website;
}
private function extractDomainByWebsite($website)
{
return $this->cleanDomain($website);
}
private function extractDomainByEmail($email)
{
return explode('@', (string) $email)[1];
}
private function getMailServices()
{
return [
"aol.com",
"aol.de",
"arcor.de",
"gmail.com",
"gmx.de",
"gmx.net",
"googlemail.com",
"hotmail.com",
"hotmail.de",
"icloud.com",
"mailbox.org",
"me.com",
"outlook.de",
"posteo.de",
"yahoo.de",
];
}
private function getDomainServices()
{
return [
"linkedin.com",
"linkedin.de",
"facebook",
];
}
public function updateLogo($startup): void
{
$startup = $this->checkLogo($startup);
if ($startup->getImportNote()) {
$suggestions = json_decode((string) $startup->getImportNote());
$exportParts = '';
$exportPartsArray = [];
foreach ($suggestions as $suggestion) {
$columns = array_values((array) $suggestion);
$exportPartsArray = [...$exportPartsArray, ...$columns];
}
$exportList[] = implode("\t", $exportPartsArray);
}
}
private function checkLogo(Startup $startup)
{
$url = "https://autocomplete.clearbit.com/v1/companies/suggest?query={$startup->getName()}";
if (!$result = $this->curl_get($url)) {
$domain = str_replace('https://', '', $startup->getWebsite());
$domain = str_replace('www.', '', $domain);
$domain = explode("/", $domain)[0];
$url = "https://autocomplete.clearbit.com/v1/companies/suggest?query={$domain}";
$result = $this->curl_get($url);
}
if ($result) {
$startup->setImportNote($result);
}
return $startup;
}
private function checkDomain(Startup $startup, $row)
{
$url = "https://autocomplete.clearbit.com/v1/companies/suggest?query={$row['domain']}";
if ($result = $this->curl_get($url)) {
$startup->setImportNote($result);
}
return $startup;
}
public function curl_get($Url, $login = false)
{
// is cURL installed yet?
if (!function_exists('curl_init')) {
die('Sorry cURL is not installed!');
}
// OK cool - then let's create a new cURL resource handle
$ch = curl_init();
// Set URL to call back
curl_setopt($ch, CURLOPT_URL, $Url);
// Should cURL return or print out the data? (true = return, false = print)
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = ['Content-Type:application/json'];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Download the given URL, and return output
$output = curl_exec($ch);
// Close the cURL resource, and free system resources
curl_close($ch);
return $output;
}
public function getTemplateVars($request)
{
$templateVars = ['filter' => null, 'tag' => []];
if ($tag = $request->query->get('tag')) {
$templateVars['tag'] = trim((string) $tag);
}
if ($location = $request->query->get('location')) {
$templateVars['location'] = $location;
}
if ($status = $request->query->get('status')) {
$status = (!in_array($status, ['active', 'inactive']) ? 'active' : $status);
$templateVars['status'] = $status;
}
if ($criteria = $request->query->get('criteria')) {
$templateVars['criteria'] = $criteria;
} else {
$templateVars['criteria'] = [];
}
if ($dimensions = $request->query->get('dimension')) {
$templateVars['dimensions'] = $dimensions;
} else {
$templateVars['dimensions'] = [];
}
if ($filter = $request->query->get('filter') and !$page = $request->get('page')) {
$templateVars['filter'] = $filter;
if (preg_match("/:/", (string) $filter)) {
$parameters = explode(':', (string) $filter);
if (isset($templateVars['criteria']['id'])) {
unset($templateVars['criteria']['id']);
}
if ($parameters[0] == 'isBookmarked') {
$templateVars['criteria'] = [];
}
if (isset($templateVars['criteria'][$parameters[0]]) and $templateVars['criteria'][$parameters[0]] == $parameters[1]) {
unset($templateVars['criteria'][$parameters[0]]);
} else {
$templateVars['criteria'][$parameters[0]] = $parameters[1];
$templateVars['filter'] = null;
}
} else {
if ($filter == 'none') {
$templateVars['criteria'] = [];
$templateVars['filter'] = null;
} else {
$templateVars['filter'] = null;
}
}
}
if ($sort = $request->query->get('sort')) {
$templateVars['sort'] = $sort;
if (preg_match("/:/", (string) $sort)) {
$parameters = explode(':', (string) $sort);
$templateVars['dimensions'][$parameters[0]] = $parameters[1];
} else {
$sort = 'none';
switch ($sort) {
default:
$templateVars['dimensions'] = [];
$templateVars['sort'] = null;
break;
}
}
}
if ($fields = $request->query->get('fields')) {
$templateVars['fields'] = $fields;
}
return $templateVars;
}
public function getAggregateByGroup($group, $criteria, $order = [], $exact = false)
{
if (empty($group)) {
return [];
}
// UP-024: Validate field exists before querying to prevent QueryException
$metadata = $this->getClassMetadata();
if (!$metadata->hasField($group)) {
return [];
}
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(['s.' . $group, 'COUNT(s.id) as number']);
$qb->from(Startup::class, 's');
if ($exact) {
$qb->where("s.{$group}>''");
}
if ($criteria) {
$qb = $this->setCriteria($qb, $criteria);
}
$qb->groupBy('s.'. $group);
if ($order) {
$metadata = $this->getClassMetadata();
foreach ($order as $key => $value) {
if ($metadata->hasField($key)) {
$qb->addOrderBy('s.' . $key, $value);
}
// UP-024: Skip non-existent fields (removed: pitchDate, etc.)
}
} else {
$qb->addOrderBy('s.'. $group, 'ASC');
}
return $qb->getQuery()->getResult();
}
// --- 1:n Team → Startup (UP-024) ---
/**
* Find all startups for a team.
*
* Since UP-024, a team can have multiple startups (1:n relationship).
*
* @return Startup[]
*/
public function findAllByTeam(Team $team): array
{
return $this->createQueryBuilder('s')
->where('s.teamId = :teamId')
->setParameter('teamId', $team->getId())
->orderBy('s.createdAt', 'DESC')
->getQuery()
->getResult();
}
/**
* Find all startups for a team by team ID.
*
* @return Startup[]
*/
public function findAllByTeamId(int $teamId): array
{
return $this->createQueryBuilder('s')
->where('s.teamId = :teamId')
->setParameter('teamId', $teamId)
->orderBy('s.createdAt', 'DESC')
->getQuery()
->getResult();
}
/**
* Count startups for a team.
*/
public function countByTeam(Team $team): int
{
return (int) $this->createQueryBuilder('s')
->select('COUNT(s.id)')
->where('s.teamId = :teamId')
->setParameter('teamId', $team->getId())
->getQuery()
->getSingleScalarResult();
}
}