<?php
namespace App\Entity;
use App\Utility\DateTimeUtility;
use App\Utility\GuidUtility;
use App\Utility\ReflectionHelper;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use ReflectionClass;
use ValueError;
/**
* @ORM\Entity(repositoryClass="App\Repository\ApplicationEventRepository")
*
* @ORM\Table(
* name="application_events",
* indexes={
*
* @ORM\Index(name="occured_at_idx", columns={"occured_at"}),
* @ORM\Index(name="event_type_occured_at_idx", columns={"event_type", "occured_at"}),
* @ORM\Index(name="request_id_occured_at_idx", columns={"request_id", "occured_at"}),
* @ORM\Index(name="session_id_occured_at_idx", columns={"session_id", "occured_at"}),
* @ORM\Index(name="jobofferer_idx", columns={"affected_user_is_jobofferer", "occured_at", "affected_user_registered_at", "event_type"}),
* @ORM\Index(name="jobseeker_idx", columns={"affected_user_is_jobseeker", "occured_at", "affected_user_registered_at", "event_type"})
* }
* )
*
* This model deliberately stores its data in a strongly denormalized form in order to allow
* efficient retrieval, searching, and grouping for statistical purposes, therefore avoiding
* cross-references.
*
* IMPORTANT: Events of this type MUST NOT be used for non-statistical uses. Specifically, DO NOT use this data to make
* functional application decisions ("if there are more than X events of type Y for a user, do Z").
* Always treat this data as if it wasn't part of the application at all - because this might well become true in the
* future, when we need to move this data into a dedicated data warehouse for large-scale statistical analytics.
*
* If you need to base application behaviour on past user behaviour,@see UsageEventService instead.
*/
class ApplicationEvent
{
public const EVENT_CATEGORY_INFO = 0; // e.g. "an email was queued for delivery", "the user submitted the profile editor with invalid entries"
public const EVENT_CATEGORY_ERROR = 1; // e.g. "could not find a user for the given user id on the billwerk return page"
public const EVENT_CATEGORY_METRIC = 2; // e.g. "notification job started and found <metric> users to notify", "notification job finished after <metric> seconds"
public const EVENT_TYPE_QUEUED_MAIL_FOR_SENDING = 0;
public const EVENT_TYPE_MAIL_DELIVERY_REPORTED = 1;
public const EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_SUCCESSFUL = 2;
public const EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_UNSUCCESSFUL = 3;
public const EVENT_TYPE_EXCEPTION = 4;
public const EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_INCOMING_DIRECT_EMAIL_COMMUNICATION = 5;
public const EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_DIRECT_EMAIL_COMMUNICATION_MESSAGE_EXCHANGE_BETWEEN_TWO_PROFILES = 6;
public const EVENT_TYPE_GOHIRING_INCOMING_NEW_FAILED = 7;
public const EVENT_TYPE_GOHIRING_INCOMING_UPDATE_FAILED = 8;
public const EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED = 9;
public const EVENT_TYPE_SELFDESCRIPTION_PROPOSAL_ASSIGNED = 10;
public const EVENT_TYPE_GERMAN_PERSONNEL_IMPORT_FAILED = 11;
public const EVENT_TYPE_REWE_IMPORT_FAILED = 12;
public const EVENT_TYPE_HAYS_IMPORT_FAILED = 13;
public const EVENT_TYPE_MYSTAFFPILOT_IMPORT_FAILED = 14;
public const EVENT_TYPE_SOFTGARDEN_IMPORT_FAILED = 15;
public const EVENT_TYPE_INVALID_MAIL_ADDRESS_FOR_MAILING_DETECTED = 16;
public const EVENT_TYPE_LENKZEIT_IMPORT_FAILED = 17;
public const EVENT_TYPE_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED = 18;
public const EVENT_TYPE_CAPTCHA_RETURNED_ERROR = 19;
public const EVENT_TYPE_ARTEMIS_IMPORT_FAILED = 20;
public const EVENT_TYPE_FLASCHENPOST_IMPORT_FAILED = 21;
public const EVENT_TYPE_DIRECT_EMAIL_COMMUNICATION_INCOMING_MESSAGE_BLOCKED_BASED_ON_KEYPHRASE = 22;
public const EVENT_TYPE_FEED_WAS_NOT_UPDATED = 23;
public const EVENT_TYPE_DEUTSCHE_BAHN_AG_IMPORT_FAILED = 24;
public const EVENT_TYPE_MILCH_UND_ZUCKER_IMPORT_FAILED = 25;
public const EVENT_TYPE_RECURRENT_JOB_SHARE_PAGE_WAS_REQUESTED_BY_BOT = 26;
public const EVENT_TYPE_CONCLUDIS_API_ERROR = 27;
public const EVENT_TYPE_KAUFLAND_IMPORT_FAILED = 28;
public const EVENT_TYPE_CELLIT_IMPORT_FAILED = 29;
public const EVENT_TYPE_CONTENT_PARSER_TASK_ERROR = 30;
public const EVENT_TYPE_AFA_UPLOAD_CHECKER_RETURNED_RESULT = 31;
public const EVENT_TYPE_ADECCO_FEED_IMPORT_FAILED = 32;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_SUCCEEDED = 33;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_ERRORED = 34;
public const EVENT_TYPE_INDEED_FEED_CREATION_FROM_FILE_RECURRENT_JOB_REJECTED = 35;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_SUCCEEDED = 36;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_ERRORED = 37;
public const EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_SUCCEEDED = 38;
public const EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_ERRORED = 39;
public const EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_QUOTA = 40;
public const EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED = 41;
public const EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_STATUS_FORBIDDEN = 42;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_SUCCEEDED = 43;
public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_ERRORED = 44;
public const EVENT_TYPE_WECLAPP_API_ERROR = 45;
public const EVENT_TYPE_SOFTGARDEN_API_ERROR = 46;
public const EVENT_TYPE_AI_AFA_ASSIGNMENT_BOTH_MATCHED = 47;
public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_MATCH = 48;
public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_PROFESSION_MATCH = 49;
public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ECONOMIC_SECTOR_MATCH = 50;
public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ASSIGNMENTS = 51;
public const EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_LIMIT_EXHAUSTED = 52;
public const ORDERED_EVENT_TYPES = [
self::EVENT_TYPE_QUEUED_MAIL_FOR_SENDING,
self::EVENT_TYPE_MAIL_DELIVERY_REPORTED,
self::EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_SUCCESSFUL,
self::EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_UNSUCCESSFUL,
self::EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_INCOMING_DIRECT_EMAIL_COMMUNICATION,
self::EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_DIRECT_EMAIL_COMMUNICATION_MESSAGE_EXCHANGE_BETWEEN_TWO_PROFILES,
self::EVENT_TYPE_GOHIRING_INCOMING_NEW_FAILED,
self::EVENT_TYPE_GOHIRING_INCOMING_UPDATE_FAILED,
self::EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED,
self::EVENT_TYPE_SELFDESCRIPTION_PROPOSAL_ASSIGNED,
self::EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED,
self::EVENT_TYPE_GERMAN_PERSONNEL_IMPORT_FAILED,
self::EVENT_TYPE_REWE_IMPORT_FAILED,
self::EVENT_TYPE_HAYS_IMPORT_FAILED,
self::EVENT_TYPE_MYSTAFFPILOT_IMPORT_FAILED,
self::EVENT_TYPE_SOFTGARDEN_IMPORT_FAILED,
self::EVENT_TYPE_INVALID_MAIL_ADDRESS_FOR_MAILING_DETECTED,
self::EVENT_TYPE_LENKZEIT_IMPORT_FAILED,
self::EVENT_TYPE_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED,
self::EVENT_TYPE_CAPTCHA_RETURNED_ERROR,
self::EVENT_TYPE_ARTEMIS_IMPORT_FAILED,
self::EVENT_TYPE_FLASCHENPOST_IMPORT_FAILED,
self::EVENT_TYPE_DIRECT_EMAIL_COMMUNICATION_INCOMING_MESSAGE_BLOCKED_BASED_ON_KEYPHRASE,
self::EVENT_TYPE_FEED_WAS_NOT_UPDATED,
self::EVENT_TYPE_DEUTSCHE_BAHN_AG_IMPORT_FAILED,
self::EVENT_TYPE_MILCH_UND_ZUCKER_IMPORT_FAILED,
self::EVENT_TYPE_RECURRENT_JOB_SHARE_PAGE_WAS_REQUESTED_BY_BOT,
self::EVENT_TYPE_CONCLUDIS_API_ERROR,
self::EVENT_TYPE_KAUFLAND_IMPORT_FAILED,
self::EVENT_TYPE_CELLIT_IMPORT_FAILED,
self::EVENT_TYPE_AFA_UPLOAD_CHECKER_RETURNED_RESULT,
self::EVENT_TYPE_ADECCO_FEED_IMPORT_FAILED,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_SUCCEEDED,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_ERRORED,
self::EVENT_TYPE_INDEED_FEED_CREATION_FROM_FILE_RECURRENT_JOB_REJECTED,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_SUCCEEDED,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_ERRORED,
self::EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_SUCCEEDED,
self::EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_ERRORED,
self::EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_QUOTA,
self::EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED,
self::EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_STATUS_FORBIDDEN,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_SUCCEEDED,
self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_ERRORED,
self::EVENT_TYPE_WECLAPP_API_ERROR,
self::EVENT_TYPE_SOFTGARDEN_API_ERROR,
self::EVENT_TYPE_AI_AFA_ASSIGNMENT_BOTH_MATCHED,
self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_MATCH,
self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_PROFESSION_MATCH,
self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ECONOMIC_SECTOR_MATCH,
self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ASSIGNMENTS,
self::EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_LIMIT_EXHAUSTED
];
/**
* @throws Exception
*/
public function __construct(
int $eventCategory,
int $eventType
) {
if (!ReflectionHelper::hasConstWithValue(
self::class,
'EVENT_CATEGORY_',
$eventCategory
)) {
throw new ValueError("Unknown event category '$eventCategory'.");
}
if (!ReflectionHelper::hasConstWithValue(
self::class,
'EVENT_TYPE_',
$eventType
)) {
throw new ValueError("Unknown event type '$eventType'.");
}
$this->eventCategory = $eventCategory;
$this->eventType = $eventType;
$this->occuredAt = DateTimeUtility::createDateTimeUtc();
}
/**
* @ORM\GeneratedValue(strategy="CUSTOM")
*
* @ORM\CustomIdGenerator(class="App\Utility\DatabaseIdGenerator")
*
* @ORM\Column(name="id", type="guid")
*
* @ORM\Id
*/
private string $id;
public function setId(string $id): void
{
GuidUtility::validOrThrow($id);
$this->id = $id;
}
public function getId(): ?string
{
return $this->id;
}
/**
* @ORM\Column(name="event_category", type="smallint", nullable=false)
*/
private int $eventCategory;
public function getEventCategory(): int
{
return $this->eventCategory;
}
/**
* @throws Exception
*/
public function getEventCategoryTitle(): string
{
$refl = new ReflectionClass(ApplicationEvent::class);
$constants = $refl->getConstants();
foreach ($constants as $constantName => $constantValue) {
if (substr($constantName, 0, 15) === 'EVENT_CATEGORY_' && $constantValue === $this->getEventCategory()) {
return strtolower(str_replace('_', '-', substr($constantName, 15)));
}
}
throw new Exception('Cannot resolve title for event category ' . $this->getEventType());
}
/**
* @ORM\Column(name="event_type", type="smallint", nullable=false)
*/
private int $eventType;
public function getEventType(): int
{
return $this->eventType;
}
/**
* @throws Exception
*/
public function getEventTypeTitle(): string
{
$refl = new ReflectionClass(ApplicationEvent::class);
$constants = $refl->getConstants();
foreach ($constants as $constantName => $constantValue) {
if (substr($constantName, 0, 11) === 'EVENT_TYPE_' && $constantValue === $this->getEventType()) {
return strtolower(str_replace('_', '-', substr($constantName, 11)));
}
}
throw new Exception('Cannot resolve title for event type ' . $this->getEventType());
}
/**
* @ORM\Column(name="occured_at", type="datetime", nullable=false)
*/
private DateTime $occuredAt;
public function setOccuredAt(DateTime $occuredAt): void
{
$this->occuredAt = $occuredAt;
}
public function getOccuredAt(): DateTime
{
return $this->occuredAt;
}
/**
* @ORM\Column(name="affected_user_id", type="guid", nullable=true)
*
* We don't make this a foreign key on purpose, we don't need the integrity and don't want to delete event data
* if a user is deleted.
*/
private ?string $affectedUserId = null;
public function setAffectedUserId(?string $userId = null): void
{
GuidUtility::validOrThrow($userId, true);
$this->affectedUserId = $userId;
}
public function getAffectedUserId(): ?string
{
return $this->affectedUserId;
}
/**
* @ORM\Column(name="affected_user_is_jobofferer", type="boolean", nullable=true)
*/
private ?bool $affectedUserIsJobofferer = null;
public function setAffectedUserIsJobofferer(?bool $affectedUserIsJobofferer): void
{
$this->affectedUserIsJobofferer = $affectedUserIsJobofferer;
}
public function getAffectedUserIsJobofferer(): ?bool
{
return $this->affectedUserIsJobofferer;
}
/**
* @ORM\Column(name="affected_user_is_jobseeker", type="boolean", nullable=true)
*/
private ?bool $affectedUserIsJobseeker = null;
public function setAffectedUserIsJobseeker(?bool $affectedUserIsJobseeker): void
{
$this->affectedUserIsJobseeker = $affectedUserIsJobseeker;
}
public function getAffectedUserIsJobseeker(): ?bool
{
return $this->affectedUserIsJobseeker;
}
/**
* @ORM\Column(name="affected_user_registered_at", type="datetime", nullable=true)
*
* In order to show statistics related to the cohorte of all users registered on day X, we need this field
*
* E.g. "from all user registered on 2018-04-07, how many ran into error X?"
*/
private ?DateTime $affectedUserRegisteredAt = null;
public function setAffectedUserRegisteredAt(?DateTime $affectedUserRegisteredAt = null): void
{
$this->affectedUserRegisteredAt = $affectedUserRegisteredAt;
}
public function getAffectedUserRegisteredAt(): ?DateTime
{
return $this->affectedUserRegisteredAt;
}
/**
* @ORM\Column(name="metric", type="float", nullable=true)
*/
private ?float $metric = null;
/**
* @throws Exception
*/
public function setMetric(?float $metric = null): void
{
$this->metric = $metric;
}
public function getMetric(): ?float
{
return $this->metric;
}
/**
* @ORM\Column(name="error_message", type="text", length=8192, nullable=true)
*/
private ?string $errorMessage = null;
/**
* @throws Exception
*/
public function setErrorMessage(?string $errorMessage = null): void
{
$this->errorMessage = $errorMessage;
}
public function getErrorMessage(): ?string
{
return $this->errorMessage;
}
/**
* @ORM\Column(name="additional_data", type="text", length=8192, nullable=true)
*/
private ?string $additionalData = null;
/**
* @throws Exception
*/
public function setAdditionalData(?string $additionalData = null): void
{
if (!is_null($additionalData)) {
if (is_null(json_decode($additionalData))) {
throw new Exception('additionalData must be valid JSON');
}
}
$this->additionalData = $additionalData;
}
public function getAdditionalData(): ?string
{
return $this->additionalData;
}
/**
* @ORM\Column(name="request_id", type="text", length=256, nullable=true)
*/
private ?string $requestId = null;
public function setRequestId(?string $requestId = null): void
{
$this->requestId = $requestId;
}
public function getRequestId(): ?string
{
return $this->requestId;
}
/**
* @ORM\Column(name="session_id", type="text", length=256, nullable=true)
*/
private ?string $sessionId = null;
public function setSessionId(?string $sessionId = null): void
{
$this->sessionId = $sessionId;
}
public function getSessionId(): ?string
{
return $this->sessionId;
}
/**
* @ORM\Column(name="client_id", type="text", length=64, nullable=true)
*/
private ?string $clientId = null;
public function setClientId(?string $clientId = null): void
{
$this->clientId = $clientId;
}
public function getClientId(): ?string
{
return $this->clientId;
}
/**
* @ORM\Column(name="is_probably_bot_request", type="boolean", nullable=true)
*/
private ?bool $isProbablyBotRequest = null;
public function getIsProbablyBotRequest(): ?bool
{
return $this->isProbablyBotRequest;
}
public function setIsProbablyBotRequest(?bool $isProbablyBotRequest): void
{
$this->isProbablyBotRequest = $isProbablyBotRequest;
}
}