api/src/Gamification/Service/GamificationManager.php
<?php
/**
* Copyright (c) 2021, MOBICOOP. All rights reserved.
* This project is dual licensed under AGPL and proprietary licence.
***************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <gnu.org/licenses>.
***************************
* Licence MOBICOOP described in the file
* LICENSE
**************************/
namespace App\Gamification\Service;
use App\Action\Entity\Action;
use App\Action\Entity\Log;
use App\Action\Repository\LogRepository;
use App\Carpool\Entity\Ask;
use App\Gamification\Entity\Badge;
use App\Gamification\Entity\GamificationAction;
use App\Gamification\Repository\SequenceItemRepository;
use App\User\Entity\User;
use App\Gamification\Entity\SequenceItem;
use App\Gamification\Entity\ValidationStep;
use App\Gamification\Repository\BadgeRepository;
use App\Gamification\Entity\BadgeProgression;
use App\Gamification\Entity\BadgeSummary;
use App\Gamification\Entity\GamificationNotifier;
use App\Gamification\Entity\Reward;
use App\Gamification\Entity\RewardStep;
use App\Gamification\Entity\SequenceStatus;
use App\Gamification\Event\BadgeEarnedEvent;
use App\Gamification\Event\RewardStepEarnedEvent;
use App\Gamification\Event\ValidationStepEvent;
use App\Gamification\Interfaces\GamificationNotificationInterface;
use App\Gamification\Resource\BadgesBoard;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use App\Gamification\Interfaces\GamificationRuleInterface;
use App\Communication\Repository\MessageRepository;
use App\Gamification\Repository\RewardStepRepository;
use App\Gamification\Repository\RewardRepository;
use App\Payment\Entity\CarpoolItem;
use Psr\Log\LoggerInterface;
use App\User\Repository\UserRepository;
use App\Gamification\Service\RetroactivelyRewardService;
use App\Gamification\Service\BadgesBoardManager;
/**
* Gamification Manager
*
* @author Maxime Bardot <maxime.bardot@mobicoop.org>
*/
class GamificationManager
{
private $sequenceItemRepository;
private $logRepository;
private $badgeRepository;
private $entityManager;
private $eventDispatcher;
private $gamificationNotifier;
private $messageRepository;
private $rewardStepRepository;
private $rewardRepository;
private $badgeImageUri;
private $logger;
private $userRepository;
private $retroactivelyRewardService;
private $badgesBoardManager;
public function __construct(
SequenceItemRepository $sequenceItemRepository,
LogRepository $logRepository,
BadgeRepository $badgeRepository,
EntityManagerInterface $entityManager,
EventDispatcherInterface $eventDispatcher,
GamificationNotifier $gamificationNotifier,
MessageRepository $messageRepository,
RewardStepRepository $rewardStepRepository,
RewardRepository $rewardRepository,
string $badgeImageUri,
LoggerInterface $logger,
UserRepository $userRepository,
RetroactivelyRewardService $retroactivelyRewardService,
BadgesBoardManager $badgesBoardManager
) {
$this->sequenceItemRepository = $sequenceItemRepository;
$this->logRepository = $logRepository;
$this->badgeRepository = $badgeRepository;
$this->entityManager = $entityManager;
$this->eventDispatcher = $eventDispatcher;
$this->gamificationNotifier = $gamificationNotifier;
$this->messageRepository = $messageRepository;
$this->rewardStepRepository = $rewardStepRepository;
$this->rewardRepository = $rewardRepository;
$this->badgeImageUri = $badgeImageUri;
$this->logger = $logger;
$this->userRepository = $userRepository;
$this->retroactivelyRewardService = $retroactivelyRewardService;
$this->badgesBoardManager = $badgesBoardManager;
}
/**
* When a new log entry is detected, we treat it to determine if there is something to do (i.e Gamification)
*
* @param Log $log Event of the action
* @return void
*/
public function handleLog(Log $log)
{
// A new log has been recorded. We need to check if there is a gamification action to take
$gamificationActions = $log->getAction()->getGamificationActions();
if (is_array($gamificationActions) && count($gamificationActions)>0) {
// This action has gamification action, we need to treat it
foreach ($gamificationActions as $gamificationAction) {
$this->treatGamificationAction($gamificationAction, $log);
}
}
}
/**
* Treatment and evaluation of a GamificationAction
*
* @param GamificationAction $gamificationAction
* @param Log $log
* @return void
*/
private function treatGamificationAction(GamificationAction $gamificationAction, Log $log)
{
// We check if this action is in a sequenceItem
$validationSteps = [];
$sequenceItems = $this->sequenceItemRepository->findBy(['gamificationAction'=>$gamificationAction]);
if (is_array($sequenceItems) && count($sequenceItems)>0) {
// This action has gamification action, we need to treat it
/**
* @var SequenceItem $sequenceItem
*/
foreach ($sequenceItems as $sequenceItem) {
$validationStep = new ValidationStep();
$validationStep->setUser($log->getUser());
$validationStep->setSequenceItem($sequenceItem);
$validationStep->setValidated(true); // By default, the sequenceItem is valid
if (!is_null($gamificationAction->getGamificationActionRule())) {
// at this point a rule is associated, we need to execute it
$gamificationActionRuleName = "\\App\\Gamification\Rule\\" . $gamificationAction->getGamificationActionRule()->getName();
/**
* @var GamificationRuleInterface $gamificationActionRule
*/
$gamificationActionRule = new $gamificationActionRuleName;
$validationStep->setValidated($validationStep->isValidated() && $gamificationActionRule->execute($log, $sequenceItem));
}
// This related action needs to be made a minimum amount of time
if (!is_null($sequenceItem->getMinCount()) && $sequenceItem->getMinCount()>0) {
$validationStep->setValidated($validationStep->isValidated() && $this->checkMinCount($gamificationAction->getAction(), $log->getUser(), $sequenceItem->getMinCount()));
}
// this related action needs to be made in a range that range date
if (($sequenceItem->isInDateRange())) {
$validationStep->setValidated($validationStep->isValidated() && $this->checkInDateRange($gamificationAction->getAction(), $log->getUser(), $sequenceItem->getBadge()->getStartDate(), $sequenceItem->getBadge()->getEndDate(), $sequenceItem->getMinCount(), $sequenceItem->getMinUniqueCount()));
}
// Dispatch an event who says that a ValidationStep has been evaluated
$validationStepEvent = new ValidationStepEvent($validationStep);
$this->eventDispatcher->dispatch(ValidationStepEvent::NAME, $validationStepEvent);
}
}
}
/**
* Check if the MinCount criteria is verified
*
* @param Action $action The action to count
* @param User $user The User we count for
* @param int $minCount The min count to be valid
* @return boolean True for valid
*/
private function checkMinCount(Action $action, User $user, int $minCount): bool
{
// We get in the log table all the Action $action made by this User $user
$logs = $this->logRepository->findBy(['action'=>$action, 'user'=>$user]);
if (is_array($logs) && count($logs)>=$minCount) {
return true;
}
return false;
}
/**
* Check if the inDateRange criteria is verified
*
* @param Action $action The action to check
* @param User $user The User who made the action
* @param DateTime $startDate The start date to be valid
* @param DateTime $endDate The end date to be valid
* @param integer $minCount The min count to be valid
* @param integer $minUniqueCount not implemented The unique min count to be vali_d
* @return boolean True for valid
*/
private function checkInDateRange(Action $action, User $user, $startDate, $endDate, $minCount=0, $minUniqueCount = 0): bool
{
// We get in the log table all the Action $action made by this User $user
$logs = $this->logRepository->findBy(['action'=>$action, 'user'=>$user]);
$logIds = [];
foreach ($logs as $log) {
if ($startDate <= $log->getDate() && $log->getDate() <= $endDate) {
$logIds[] = $log->getId();
}
}
if (count($logIds)>$minCount) {
return true;
}
return false;
}
/**
* Get the Badges earned by a User
*
* @param User $user
* @return array|null
*/
public function getBadgesEarned(User $user): ?array
{
$badges = [];
foreach ($user->getRewards() as $reward) {
$badges[] = $reward->getBadge();
}
return $badges;
}
/**
* Take a ValidationStep and take the necessary actions about it (RewardStep, Badge...)
*
* @param ValidationStep $validationStep The ValidationStep to treat
* @return void
*/
public function handleValidationStep(ValidationStep $validationStep)
{
if ($validationStep->isValidated()) {
// The ValidationStep has been validated
// First we get the BadgesBoard of this User. With it, we can check if this particular step has alteady been validated
$badgesBoard = $this->badgesBoardManager->getBadgesBoard($validationStep->getUser());
foreach ($badgesBoard->getBadges() as $badgeProgression) {
$badgeSummary = $badgeProgression->getBadgeSummary();
$currentSequenceValidation = []; // We will store the status of every SequenceItem
$newValidation = false;
foreach ($badgeSummary->getSequences() as $sequenceStatus) {
// We found the right sequence
if ($sequenceStatus->getSequenceItemId() == $validationStep->getSequenceItem()->getId()) {
// If it's a new validation, We store it be inserting a line in RewardStep for the User
if (!$sequenceStatus->isValidated()) {
$newValidation = true;
$rewardStep = new RewardStep();
$rewardStep->setUser($validationStep->getUser());
$validationStep->getSequenceItem()->addRewardStep($rewardStep);
$this->entityManager->persist($validationStep->getSequenceItem());
// We also update the current SequenceStatus to evaluate further it this is enough to earn badge
$sequenceStatus->setValidated(true);
// Dispatch the event
$rewardStepEarnedEvent = new RewardStepEarnedEvent($rewardStep);
$this->eventDispatcher->dispatch(RewardStepEarnedEvent::NAME, $rewardStepEarnedEvent);
}
}
// We store the status of the current SequenceItem. If all validated, maybe the user earned a Badge
$currentSequenceValidation[] = $sequenceStatus->isValidated();
}
if (!in_array(false, $currentSequenceValidation)) {
// All steps are valid !
if ($newValidation) {
// There was a new validation, a new Badge is earned !
// We get the badge involved and add a User owning this Badge (add a line in Reward table)
$badge = $this->badgeRepository->find($badgeSummary->getBadgeId());
$reward = new Reward();
$reward->setUser($validationStep->getUser());
$badge->addReward($reward);
$this->entityManager->persist($badge);
// Dispatch the event
$badgeEarnedEvent = new BadgeEarnedEvent($reward);
$this->eventDispatcher->dispatch(BadgeEarnedEvent::NAME, $badgeEarnedEvent);
}
}
}
$this->entityManager->flush();
}
}
/**
* Add a Gamification notification to the current pool that will be return at the end of the request
*
* @param GamificationNotificationInterface $gamificationNotification
* @return void
*/
public function handleGamificationNotification(GamificationNotificationInterface $gamificationNotification)
{
$this->gamificationNotifier->addNotification($gamificationNotification);
}
/**
* Tag a RewardStep as notified
*
* @param int $id Id of the RewardStep to tag
* @return RewardStep
*/
public function tagRewardStepAsNotified(int $id): RewardStep
{
if ($rewardStep = $this->rewardStepRepository->find($id)) {
$rewardStep->setNotifiedDate(new \DateTime('now'));
$this->entityManager->persist($rewardStep);
$this->entityManager->flush();
return $rewardStep;
}
throw new \LogicException("No RewardStep found");
}
/**
* Tag a Reward as notified
*
* @param int $id Id of the RewardStep to tag
* @return Reward
*/
public function tagRewardAsNotified(int $id): Reward
{
if ($reward = $this->rewardRepository->find($id)) {
$reward->setNotifiedDate(new \DateTime('now'));
$this->entityManager->persist($reward);
$this->entityManager->flush();
return $reward;
}
throw new \LogicException("No Reward found");
}
public function retroactivelyGenerateRewards()
{
return $this->retroactivelyRewardService->retroactivelyRewardUsers();
}
}