<?php
namespace Products\NotificationsBundle\Entity;
use App\Entity\System\School;
use App\Model\Async\StringSemaphoreInterface;
use App\Model\Async\StringSemaphoreTrait;
use App\Model\Query\ConditionQuery\Condition\ContainsCondition;
use App\Model\Query\ConditionQuery\Condition\EqualCondition;
use App\Model\Query\ConditionQuery\Condition\LessThanCondition;
use App\Model\Query\ConditionQuery\Condition\NullCondition;
use App\Model\Query\ConditionQuery\ConditionGroup;
use App\Model\Query\ConditionQuery\ConditionGroupInterface;
use App\Model\Query\ConditionQuery\ConditionQuery;
use App\Model\Schedule\ScheduleInterface;
use App\Model\Schedule\ScheduleTrait;
use Cms\CoreBundle\Model\Interfaces\Loggable\LoggableInterface;
use Cms\TenantBundle\Entity\TenantedEntity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Products\NotificationsBundle\Entity\Notifications\Invocation;
use Products\NotificationsBundle\Entity\Notifications\Template;
use Products\NotificationsBundle\Model\AutomationList;
use Products\NotificationsBundle\Util\ListBuilder\AbstractListBuilder;
use Reinder83\BinaryFlags\Bits;
/**
*
* @ORM\Entity(
* repositoryClass = "Products\NotificationsBundle\Doctrine\Repository\AutomationRepository",
* )
* @ORM\Table(
* name = "notis__automation",
* )
*/
class Automation extends TenantedEntity implements LoggableInterface, StringSemaphoreInterface, ScheduleInterface
{
public const TYPES__GENERAL = 0;
public const TYPES__ATTENDANCE = Bits::BIT_1;
public const TYPES = [
'general' => self::TYPES__GENERAL,
'attendance' => self::TYPES__ATTENDANCE,
];
use StringSemaphoreTrait;
use ScheduleTrait;
/**
* @var bool
*
* @ORM\Column(
* type = "boolean",
* nullable = false,
* options = {
* "default" = true,
* },
* )
*/
protected bool $active = true;
/**
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = false,
* length = 255,
* )
*/
protected ?string $name = null;
/**
* @var School|null
*
* @ORM\ManyToOne(
* targetEntity = School::class,
* )
* @ORM\JoinColumn(
* name = "school",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "SET NULL",
* )
*/
protected ?School $school = null;
/**
* @var ConditionQuery|null
*
* @ORM\Column(
* type = "condition_query",
* nullable = false,
* )
*/
protected ?ConditionQuery $conditionQuery = null;
/**
* @var Template|null
*
* @ORM\ManyToOne(
* targetEntity = Template::class,
* )
* @ORM\JoinColumn(
* name = "template",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "SET NULL",
* )
*/
protected ?Template $template = null;
/**
* @var bool
*
* @ORM\Column(
* type = "boolean",
* options = {
* "default" = false,
* },
* )
*/
protected bool $trackable = false;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* options = {
* "default" = 0,
* },
* )
*/
protected int $trackableWindow = 0;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = self::TYPES__GENERAL,
* "unsigned" = true,
* },
* )
*/
protected int $type = self::TYPES__GENERAL;
/**
* @var Collection|Invocation[]
*
* @ORM\OneToMany(
* targetEntity = Invocation::class,
* mappedBy = "automation",
* )
* @ORM\OrderBy({
* "createdAt" = "DESC",
* })
*/
protected Collection $invocations;
public function __construct()
{
$this->invocations = new ArrayCollection();
}
/**
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string|null $name
* @return self
*/
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
/**
* @return School|null
*/
public function getSchool(): ?School
{
return $this->school;
}
/**
* @param School|null $school
* @return self
*/
public function setSchool(?School $school): self
{
$this->school = $school;
return $this;
}
/**
* @return bool
*/
public function isActive(): bool
{
return $this->active;
}
/**
* @param bool $active
* @return self
*/
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
/**
* This sets the "raw" query that the user defines.
* For most purposes, you likely want to use the "getRunnableConditionQuery" method.
*
* @return ConditionQuery|null
* @internal
*/
public function getConditionQuery(): ?ConditionQuery
{
return $this->conditionQuery;
}
/**
* @param ConditionQuery|null $conditionQuery
* @return self
*/
public function setConditionQuery(?ConditionQuery $conditionQuery): self
{
$this->conditionQuery = $conditionQuery;
return $this;
}
/**
* Takes the user-provided query and performs any final "transformations" to it.
* This adds things like the additional pieces required to run "trackable" automations, etc.
*
* @return ConditionQuery|null
*/
public function getRunnableConditionQuery(): ?ConditionQuery
{
// get teh base query
$conditionQuery = $this->getConditionQuery();
if ( ! $conditionQuery instanceof ConditionQuery) {
return null;
}
// determine if the school needs added
// attach it if needed
$school = $this->getSchool();
if ($school && ! $school->isTypeDistrict() && $school->getOneRosterOrg()) {
$conditionQuery = new ConditionQuery(
$conditionQuery->getEntity(),
[
new ContainsCondition(
sprintf(
'%s.metadata/_orgs',
$conditionQuery->getEntityAlias(),
),
[$school->getOneRosterOrg()]
),
new ConditionGroup(
$conditionQuery->getConditions(),
$conditionQuery->getMode()
),
],
ConditionGroupInterface::MODES__AND
);
}
// if this is not a trackable query, then we are done
if ( ! $this->isTrackable()) {
return $conditionQuery;
}
// only profile condition query is "trackable"
if ($conditionQuery->getEntity() !== ConditionQuery::PROFILE_ENTITY) {
return $conditionQuery;
}
$trackableDate = null;
if ($this->getTrackableWindow() > 0) {
// TODO: should we be timestamping people based on the start of the day instead of using NOW()?
// TODO: does customer timezone have any affect on this calculation?
$trackableDate = new DateTime(
sprintf(
'-%d days',
$this->getTrackableWindow()
)
);
}
// if window equals zero: (automations.automation IS NULL ) AND (...other clauses generated from customers query...)
// if window greater than zero: (automations.automation IS NULL OR (automations.automation = automationId AND automations.timestamp < :timestamp)) AND (...other clauses generated from customers query...)
return new ConditionQuery(
$conditionQuery->getEntity(),
[
new ConditionGroup(
array_filter([
new NullCondition(
AbstractListBuilder::ENTITIES__PROFILE_AUTOMATION_RECORDS . '.automation',
),
// if trackable window is 0 (i.e. no date calculated), we send only once
// so, the following condition group is not needed for a window of 0
$trackableDate ? new ConditionGroup(
[
new EqualCondition(
AbstractListBuilder::ENTITIES__PROFILE_AUTOMATION_RECORDS . '.automation',
$this->getId()
),
new LessThanCondition(
AbstractListBuilder::ENTITIES__PROFILE_AUTOMATION_RECORDS . '.timestamp',
$trackableDate->format('Y-m-d H:m:i')
),
],
ConditionGroupInterface::MODES__AND,
) : null,
]), ConditionGroupInterface::MODES__OR
),
new ConditionGroup(
$conditionQuery->getConditions(),
$conditionQuery->getMode()
),
],
ConditionGroupInterface::MODES__AND
);
}
/**
* @return Template|null
*/
public function getTemplate(): ?Template
{
return $this->template;
}
/**
* @param Template|null $template
* @return self
*/
public function setTemplate(?Template $template): self
{
$this->template = $template;
return $this;
}
/**
* Returns a ConditionList that uses the "runnable" condition query for the automation.
* This is the condition query that applies other logic (like "trackable" settings) to whatever the user has provided.
* If a condition query hasn't been set yet, this returns NULL.
*
* @return AutomationList|null
*/
public function getRunnableConditionList(): ?AutomationList
{
$conditionQuery = $this->getRunnableConditionQuery();
if ( ! $conditionQuery instanceof ConditionQuery) {
return null;
}
return (new AutomationList($conditionQuery))
->setTenant($this->getTenant());
}
/**
* @return bool
*/
public function isTrackable(): bool
{
return $this->trackable;
}
/**
* @param bool $trackable
* @return self
*/
public function setTrackable(bool $trackable): self
{
$this->trackable = $trackable;
return $this;
}
/**
* @return int
*/
public function getTrackableWindow(): int
{
return $this->trackableWindow;
}
/**
* @param int $trackableWindow
* @return self
*/
public function setTrackableWindow(int $trackableWindow): self
{
$this->trackableWindow = max(0, $trackableWindow);
return $this;
}
/**
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* @param int $type
* @return self
*/
public function setType(int $type): self
{
$this->type = $type;
return $this;
}
/**
* {@inheritDoc}
*/
public function getLoggableDetails(): array
{
return [
'id' => (string)$this->getId(),
'title' => $this->getName(),
];
}
/**
* @param Criteria|null $criteria
* @return Collection|Invocation[]
*/
public function getInvocations(?Criteria $criteria = null): Collection
{
if ( ! empty($criteria)) {
return $this->invocations->matching($criteria);
}
return $this->invocations;
}
/**
* @return Invocation|null
*/
public function getLastInvocation(): ?Invocation
{
if ($this->invocations->isEmpty()) {
return null;
}
return $this->getInvocations(new Criteria(null, ['createdAt' => 'DESC'], null, 1))->first();
}
/**
* @param Collection $invocations
* @return self
*/
public function setInvocations(Collection $invocations): self
{
$this->invocations = $invocations;
return $this;
}
}