src/Entity/Activity.php line 39

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Entity;
  4. use App\Contracts\Activity\ActivityBoostStatus;
  5. use App\Contracts\Activity\Status;
  6. use App\Contracts\Platform\Platform;
  7. use App\Entity\Lite\CreatorLite;
  8. use App\Repository\ActivityRepository;
  9. use App\Services\DraftCampaign\Contract\DraftVideoStatus;
  10. use DateTime;
  11. use DateTimeImmutable;
  12. use Doctrine\Common\Collections\ArrayCollection;
  13. use Doctrine\Common\Collections\Collection;
  14. use Doctrine\Common\Collections\Criteria;
  15. use Doctrine\ORM\Mapping\Column;
  16. use Doctrine\ORM\Mapping\Entity;
  17. use Doctrine\ORM\Mapping\GeneratedValue;
  18. use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
  19. use Doctrine\ORM\Mapping\Id;
  20. use Doctrine\ORM\Mapping\Index;
  21. use Doctrine\ORM\Mapping\JoinColumn;
  22. use Doctrine\ORM\Mapping\ManyToOne;
  23. use Doctrine\ORM\Mapping\OneToMany;
  24. use Doctrine\ORM\Mapping\OneToOne;
  25. use Doctrine\ORM\Mapping\PrePersist;
  26. use Doctrine\ORM\Mapping\PreUpdate;
  27. use Doctrine\ORM\Mapping\UniqueConstraint;
  28. use Symfony\Component\String\AbstractString;
  29. use function Symfony\Component\String\s;
  30. #[Entity(ActivityRepository::class)]
  31. #[Index(fields: ['status''updatedAt'])]
  32. #[UniqueConstraint('unique_activity', ['creator_id''campaign_id'])]
  33. #[HasLifecycleCallbacks]
  34. class Activity
  35. {
  36.     /** @var int */
  37.     private const READ_MORE_LENGTH 35;
  38.     /** @var int */
  39.     private const DEFAULT_AMOUNT 0;
  40.     #[Id]
  41.     #[GeneratedValue]
  42.     #[Column('id''bigint'options: ['unsigned' => true])]
  43.     private ?int $id null;
  44.     #[ManyToOne(targetEntityCreator::class, inversedBy'activities')]
  45.     #[JoinColumn(onDelete'CASCADE')]
  46.     private Creator|CreatorLite $creator;
  47.     #[Column(
  48.         type'bigint',
  49.         options: [
  50.             'unsigned' => true,
  51.             'comment' => 'Field to specify the user responsible for processing the activity',
  52.         ],
  53.     )]
  54.     private ?int $curatorUserId null;
  55.     #[Column(type'datetime_immutable'nullabletrue)]
  56.     private ?DateTimeImmutable $curatedAt null;
  57.     #[ManyToOne(targetEntityCampaign::class, inversedBy'activities')]
  58.     private Campaign $campaign;
  59.     #[OneToOne(targetEntityVideo::class)]
  60.     private ?Video $video null;
  61.     #[OneToOne(targetEntityComplaint::class, cascade: ['persist'])]
  62.     #[JoinColumn(onDelete'SET NULL')]
  63.     private ?Complaint $complaint null;
  64.     #[Column(name'boost_status'type'smallint'nullablefalseoptions: [
  65.         'comment' => 'Status related to the boost code request from creator',
  66.     ])]
  67.     private int $boostStatus ActivityBoostStatus::NONE;
  68.     #[Column('boost_requested_by_admin''boolean'nullablefalseoptions: ['default' => false])]
  69.     private bool $boostRequestedByAdmin false;
  70.     #[OneToMany(mappedBy'activity'targetEntityVideoPending::class)]
  71.     private Collection $pendingVideos;
  72.     #[OneToMany(mappedBy'activity'targetEntityDraftVideo::class)]
  73.     private Collection $draftVideos;
  74.     #[Column(name'status'type'smallint')]
  75.     private int $status Status::INBOX;
  76.     #[Column('amount''float'options: ['unsigned' => true'default' => self::DEFAULT_AMOUNT])]
  77.     private float $amount self::DEFAULT_AMOUNT;
  78.     #[Column(type'float'precision10scale2nullablefalseoptions: ['unsigned' => true])]
  79.     private float $cpm;
  80.     #[Column('paid''float'options: ['unsigned' => true'default' => self::DEFAULT_AMOUNT])]
  81.     private float $paid self::DEFAULT_AMOUNT;
  82.     #[Column('personal_bonus''float'options: ['unsigned' => true'default' => self::DEFAULT_AMOUNT])]
  83.     private float $personalBonus self::DEFAULT_AMOUNT;
  84.     #[Column('followers''bigint'nullabletrueoptions: ['unsigned' => true])]
  85.     private ?int $followers null;
  86.     #[Column('averageViews''bigint'nullabletrueoptions: ['unsigned' => true])]
  87.     private ?int $averageViews null;
  88.     #[Column('vip_multiplier''float'nullabletrueoptions: ['unsigned' => true'default' => null])]
  89.     private ?float $vipMultiplier null;
  90.     #[Column(type'text')]
  91.     private string $reason '';
  92.     #[Column(type'text'nullabletrue)]
  93.     private ?string $disputeReason '';
  94.     #[Column(
  95.         name'video_deadline_at',
  96.         type'datetime_immutable',
  97.         nullabletrue,
  98.         options: [
  99.             'comment' => 'End date for adding a video to the activity',
  100.         ],
  101.     )]
  102.     private ?DateTimeImmutable $videoDeadlineAt null;
  103.     #[Column(name'created_at'type'datetime')]
  104.     private ?DateTime $createdAt null;
  105.     #[Column(name'updated_at'type'datetime'nullabletrue)]
  106.     private ?DateTime $updatedAt null;
  107.     #[Column('payed_at''datetime'nullabletrue)]
  108.     private ?DateTime $payedAt null;
  109.     public function __construct()
  110.     {
  111.         $this->pendingVideos = new ArrayCollection();
  112.         $this->draftVideos = new ArrayCollection();
  113.     }
  114.     #[PrePersist]
  115.     public function prePersist(): void
  116.     {
  117.         $this->createdAt = new DateTime();
  118.     }
  119.     #[PreUpdate]
  120.     public function preUpdate(): void
  121.     {
  122.         $this->updatedAt = new DateTime();
  123.     }
  124.     public function getId(): ?int
  125.     {
  126.         return $this->id;
  127.     }
  128.     public function getCreator(): Creator
  129.     {
  130.         return $this->creator;
  131.     }
  132.     public function setCreator(Creator $creator): self
  133.     {
  134.         $this->creator $creator;
  135.         return $this;
  136.     }
  137.     public function getCpm(): float
  138.     {
  139.         return $this->cpm;
  140.     }
  141.     public function setCpm(float $cpm): static
  142.     {
  143.         $this->cpm $cpm;
  144.         return $this;
  145.     }
  146.     public function getCuratorUserId(): ?int
  147.     {
  148.         return $this->curatorUserId;
  149.     }
  150.     public function setCuratorUserId(?int $curatorUserId): self
  151.     {
  152.         $this->curatorUserId $curatorUserId;
  153.         return $this;
  154.     }
  155.     public function getCuratedAt(): ?DateTimeImmutable
  156.     {
  157.         return $this->curatedAt;
  158.     }
  159.     public function setCuratedAt(?DateTimeImmutable $curatedAt): void
  160.     {
  161.         $this->curatedAt $curatedAt;
  162.     }
  163.     public function getCampaign(): Campaign
  164.     {
  165.         return $this->campaign;
  166.     }
  167.     public function resolvePlatform(): Platform
  168.     {
  169.         $platform $this->campaign->getPlatform() ?? $this->creator->getPlatform();
  170.         if ($platform === null) {
  171.             throw new \RuntimeException(
  172.                 sprintf('Activity #%s has no platform on campaign or creator.'$this->id ?? 'unknown'),
  173.             );
  174.         }
  175.         return $platform;
  176.     }
  177.     public function setCampaign(Campaign $campaign): self
  178.     {
  179.         $this->campaign $campaign;
  180.         return $this;
  181.     }
  182.     public function setStatus(int $status): self
  183.     {
  184.         $this->status $status;
  185.         return $this;
  186.     }
  187.     public function getStatus(): int
  188.     {
  189.         return $this->status;
  190.     }
  191.     public function isHideAudio(): bool
  192.     {
  193.         return $this->getCampaign()->isHideAudio()
  194.             && ($this->isReviewed() || $this->isInbox());
  195.     }
  196.     public function isInbox(): bool
  197.     {
  198.         return Status::INBOX === $this->status;
  199.     }
  200.     public function isAccepted(): bool
  201.     {
  202.         return in_array($this->status, [
  203.             Status::ACCEPTED,
  204.             Status::SPONSOR_REVIEW,
  205.             Status::VIDEO_PENDING,
  206.         ], true);
  207.     }
  208.     public function isActive(): bool
  209.     {
  210.         return Status::ACTIVE === $this->status;
  211.     }
  212.     public function isReviewed(): bool
  213.     {
  214.         return Status::SPONSOR_REVIEW === $this->status;
  215.     }
  216.     public function isVideoPending(): bool
  217.     {
  218.         return Status::VIDEO_PENDING === $this->status;
  219.     }
  220.     public function isVideoReview(): bool
  221.     {
  222.         return Status::VIDEO_REVIEW === $this->status;
  223.     }
  224.     public function isDraftVideoReview(): bool
  225.     {
  226.         return Status::DRAFT_VIDEO_REVIEW === $this->status;
  227.     }
  228.     public function isCompleted(): bool
  229.     {
  230.         return Status::COMPLETED === $this->status;
  231.     }
  232.     public function isDeclined(): bool
  233.     {
  234.         return in_array($this->status, [
  235.             Status::DECLINE,
  236.             Status::SPONSOR_DECLINE,
  237.         ], true);
  238.     }
  239.     public function isSponsorDeclined(): bool
  240.     {
  241.         return Status::SPONSOR_DECLINE === $this->status;
  242.     }
  243.     public function accept(int $videoExecutionLimitInDays 0, ?float $vipMultiplier nullint $newStatus Status::ACCEPTED): self
  244.     {
  245.         $this->followers $this->getCreator()->getFollowers();
  246.         $this->averageViews $this->getCreator()->getAverageViews();
  247.         $this->status $newStatus;
  248.         $this->vipMultiplier $vipMultiplier;
  249.         if ($videoExecutionLimitInDays 0) {
  250.             $this->videoDeadlineAt = (new DateTimeImmutable())->modify(sprintf('+%d days'$videoExecutionLimitInDays));
  251.         }
  252.         return $this;
  253.     }
  254.     public function decline(bool $bySponsor false): self
  255.     {
  256.         $this->status $bySponsor Status::SPONSOR_DECLINE Status::DECLINE;
  257.         return $this;
  258.     }
  259.     public function activate(): self
  260.     {
  261.         $this->status Status::ACTIVE;
  262.         return $this;
  263.     }
  264.     public function videoPending(): self
  265.     {
  266.         $this->status Status::VIDEO_PENDING;
  267.         return $this;
  268.     }
  269.     public function review(): self
  270.     {
  271.         $this->status Status::SPONSOR_REVIEW;
  272.         return $this;
  273.     }
  274.     public function complete(): self
  275.     {
  276.         $this->status Status::COMPLETED;
  277.         return $this;
  278.     }
  279.     public function setVideo(Video $video): self
  280.     {
  281.         $this->video $video;
  282.         return $this;
  283.     }
  284.     public function getVideo(): ?Video
  285.     {
  286.         return $this->video;
  287.     }
  288.     public function getComplaint(): ?Complaint
  289.     {
  290.         return $this->complaint;
  291.     }
  292.     public function setComplaint(?Complaint $complaint): self
  293.     {
  294.         $this->complaint $complaint;
  295.         return $this;
  296.     }
  297.     public function isBoostCodeNotRequested(): bool
  298.     {
  299.         return ActivityBoostStatus::NONE === $this->boostStatus;
  300.     }
  301.     public function isBoostCodeRequested(): bool
  302.     {
  303.         return ActivityBoostStatus::REQUESTED === $this->boostStatus;
  304.     }
  305.     public function isBoostCodeReceived(): bool
  306.     {
  307.         return ActivityBoostStatus::RECEIVED === $this->boostStatus;
  308.     }
  309.     public function isBoostCodeRequestedByAdmin(): bool
  310.     {
  311.         return $this->boostRequestedByAdmin;
  312.     }
  313.     public function setBoostRequestedByAdmin(bool $boostRequestedByAdmin): self
  314.     {
  315.         $this->boostRequestedByAdmin $boostRequestedByAdmin;
  316.         return $this;
  317.     }
  318.     public function getBoostStatus(): int
  319.     {
  320.         return $this->boostStatus;
  321.     }
  322.     public function setBoostStatus(int $boostStatus): self
  323.     {
  324.         $this->boostStatus $boostStatus;
  325.         return $this;
  326.     }
  327.     public function getVideoDeadlineAt(): ?DateTimeImmutable
  328.     {
  329.         return $this->videoDeadlineAt;
  330.     }
  331.     public function setVideoDeadlineAt(?DateTimeImmutable $videoDeadlineAt): void
  332.     {
  333.         $this->videoDeadlineAt $videoDeadlineAt;
  334.     }
  335.     public function getCreatedAt(): ?DateTime
  336.     {
  337.         return $this->createdAt;
  338.     }
  339.     public function getUpdatedAt(): ?DateTime
  340.     {
  341.         return $this->updatedAt;
  342.     }
  343.     public function getInstructions(): AbstractString
  344.     {
  345.         return s($this->getCampaign()->getInstructions());
  346.     }
  347.     public function isReadMore(): bool
  348.     {
  349.         return $this->getCampaign()->isPredefinedInstructions()
  350.             && $this->getInstructions()->length() > self::READ_MORE_LENGTH;
  351.     }
  352.     public function getShortInstructions(): string
  353.     {
  354.         return $this->getInstructions()->truncate(self::READ_MORE_LENGTHcutfalse)->toString();
  355.     }
  356.     public function setAmount(float $amount): self
  357.     {
  358.         $this->amount $amount;
  359.         return $this;
  360.     }
  361.     public function getAmount(): float
  362.     {
  363.         return $this->amount;
  364.     }
  365.     public function setPaid(float $paid): self
  366.     {
  367.         $this->paid $paid;
  368.         return $this;
  369.     }
  370.     public function getPaid(): float
  371.     {
  372.         return $this->paid;
  373.     }
  374.     public function getPayedAt(): ?DateTime
  375.     {
  376.         return $this->payedAt;
  377.     }
  378.     public function confirmPayout(): self
  379.     {
  380.         $this->payedAt = new DateTime();
  381.         return $this;
  382.     }
  383.     public function isNeedPayment(): bool
  384.     {
  385.         return $this->isCompleted() && $this->getPayedAt() === null;
  386.     }
  387.     public function videoPaid(): bool
  388.     {
  389.         return Status::COMPLETED === $this->getStatus() && $this->getPayedAt() !== null;
  390.     }
  391.     public function setFollowers(int $followers): self
  392.     {
  393.         $this->followers $followers;
  394.         return $this;
  395.     }
  396.     public function getFollowers(): int
  397.     {
  398.         return $this->followers ?? $this->getCreator()->getFollowers();
  399.     }
  400.     public function getRawFollowers(): ?int
  401.     {
  402.         return $this->followers;
  403.     }
  404.     public function setAverageViews(int $averageViews): self
  405.     {
  406.         $this->averageViews $averageViews;
  407.         return $this;
  408.     }
  409.     public function getAverageViews(): int
  410.     {
  411.         return $this->averageViews ?? $this->getCreator()->getAverageViews();
  412.     }
  413.     public function getVipMultiplier(): ?float
  414.     {
  415.         return $this->vipMultiplier;
  416.     }
  417.     public function setVipMultiplier(?float $vipMultiplier): self
  418.     {
  419.         $this->vipMultiplier $vipMultiplier;
  420.         return $this;
  421.     }
  422.     public function getRawAverageViews(): ?int
  423.     {
  424.         return $this->averageViews;
  425.     }
  426.     public function getPersonalBonus(): float
  427.     {
  428.         return $this->personalBonus;
  429.     }
  430.     public function setPersonalBonus(float $personalBonus): self
  431.     {
  432.         $this->personalBonus $personalBonus;
  433.         return $this;
  434.     }
  435.     public function isSetPersonalBonus(): bool
  436.     {
  437.         return $this->getPersonalBonus() > 0;
  438.     }
  439.     public function getReason(): string
  440.     {
  441.         return $this->reason;
  442.     }
  443.     public function setReason(string $reason): self
  444.     {
  445.         $this->reason $reason;
  446.         return $this;
  447.     }
  448.     public function getDisputeReason(): ?string
  449.     {
  450.         return $this->disputeReason;
  451.     }
  452.     public function setDisputeReason(?string $disputeReason): self
  453.     {
  454.         $this->disputeReason $disputeReason;
  455.         return $this;
  456.     }
  457.     public function getPendingVideos(): Collection
  458.     {
  459.         return $this->pendingVideos;
  460.     }
  461.     public function getDraftVideos(): Collection
  462.     {
  463.         return $this->draftVideos;
  464.     }
  465.     public function getPendingDraftVideo(): ?DraftVideo
  466.     {
  467.         $video $this->getDraftVideos()->filter(function ($video) {
  468.             return $video->getStatus() == DraftVideoStatus::PENDING;
  469.         })->first();
  470.         if ($video === false) {
  471.             return null;
  472.         }
  473.         return $video;
  474.     }
  475.     public function getApprovedDraftVideo(): ?DraftVideo
  476.     {
  477.         $video $this->getDraftVideos()->filter(function ($video) {
  478.             return $video->getStatus() == DraftVideoStatus::APPROVED;
  479.         })->first();
  480.         if ($video === false) {
  481.             return null;
  482.         }
  483.         return $video;
  484.     }
  485.     public function getActualDraftVideo(): ?DraftVideo
  486.     {
  487.         if ($this->isDraftVideoReview()) {
  488.             return $this->getPendingDraftVideo();
  489.         }
  490.         return $this->getApprovedDraftVideo();
  491.     }
  492.     public function isDraftVideoRequired(): bool
  493.     {
  494.         return
  495.             $this->getStatus() == Status::ACCEPTED &&
  496.             $this->getActualDraftVideo() === null &&
  497.             $this->getCampaign()->isDraftVideo();
  498.     }
  499.     public function getActualModifyDraftVideo(): ?DraftVideo
  500.     {
  501.         if ($this->getActualDraftVideo() !== null) {
  502.             return null;
  503.         }
  504.         return $this->getLastModifyDraftVideo();
  505.     }
  506.     public function getLastModifyDraftVideo(): ?DraftVideo
  507.     {
  508.         if ($this->getActualDraftVideo() !== null) {
  509.             return null;
  510.         }
  511.         $declinedVideos $this->getDraftVideos()->filter(function ($video) {
  512.             return $video->getStatus() === DraftVideoStatus::MODIFY;
  513.         });
  514.         if ($declinedVideos->isEmpty()) {
  515.             return null;
  516.         }
  517.         $sortedVideos $declinedVideos->matching(
  518.             Criteria::create()->orderBy(['id' => Criteria::DESC])
  519.         );
  520.         return $sortedVideos->first();
  521.     }
  522.     /**
  523.      * Average-view rate: a creator-account quality proxy (average views / followers).
  524.      * Historically — and misleadingly — surfaced as "engagement rate" on creator-screening
  525.      * surfaces (creator review, targeting/eligibility, analytics). This is NOT post
  526.      * engagement and must not be confused with {@see getEngagementRate()}.
  527.      * See docs/EngagementRateMetric.md.
  528.      */
  529.     public function getAverageViewRate(): float
  530.     {
  531.         if ($this->followers === 0) {
  532.             return 0;
  533.         }
  534.         return $this->averageViews $this->followers;
  535.     }
  536.     /**
  537.      * Post engagement rate for this activity's video: (likes + comments + shares) / views.
  538.      * This is the campaign-performance engagement metric shown on results dashboards.
  539.      * Returns 0.0 when there is no video or the video has zero views.
  540.      * See docs/EngagementRateMetric.md.
  541.      */
  542.     public function getEngagementRate(): float
  543.     {
  544.         $video $this->getVideo();
  545.         if ($video === null || $video->getViews() === 0) {
  546.             return 0.0;
  547.         }
  548.         return ($video->getLikes() + $video->getComments() + $video->getShares()) / $video->getViews();
  549.     }
  550. }