Skip to content

Commit

Permalink
DPMMA-2550 Point Groups API (#13517)
Browse files Browse the repository at this point in the history
* feat: [DPMMA-2550] point groups api

* feat: [DPMMA-2550] contact group score api

* feat: [DPMMA-2550] contact group score api changelog

* feat: [DPMMA-2550] contact group score api tests

* feat: [DPMMA-2550] contact single group score api and refactor

* feat: [DPMMA-2550] change group entity name in api

* fix: [DPMMA-2550] fix rector for PointGroupsApiController

* fix: [DPMMA-2550] point groups api improvements and extra tests

---------

Co-authored-by: Ruth Cheesley <ruth@ruthcheesley.co.uk>
Co-authored-by: John Linhart <admin@escope.cz>
  • Loading branch information
3 people committed Apr 29, 2024
1 parent 4a80f1b commit f86c50e
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 0 deletions.
4 changes: 4 additions & 0 deletions app/bundles/LeadBundle/Translations/en_US/messages.ini
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ mautic.lead.field.utmmedium="Medium"
mautic.lead.field.umtsource="Source"
mautic.lead.field.utmterm="Term"
mautic.lead.event.api="API"
mautic.lead.event.api.operation.not.allowed="The requested operation is not permitted."
mautic.lead.event.api.lead.not.found="Contact could not be found."
mautic.lead.event.api.point.group.not.found="The specified point group was not found."
mautic.lead.event.apiadded="Added through API"
mautic.lead.field.form.choose="Select a field"
mautic.lead.field.form.confirmbatchdelete="Delete the selected custom fields? WARNING: this will also delete any values of these custom fields that are associated with contacts."
Expand Down Expand Up @@ -415,6 +418,7 @@ mautic.lead.lead.submitaction.operator_divide="divide (/)"
mautic.lead.lead.submitaction.operator_minus="subtract (-)"
mautic.lead.lead.submitaction.operator_plus="add (+)"
mautic.lead.lead.submitaction.operator_times="multiply (x)"
mautic.lead.lead.submitaction.operator_set="set"
mautic.lead.lead.submitaction.points="Amount to change points by"
mautic.lead.lead.tab.history="History"
mautic.lead.lead.tab.notes="Notes"
Expand Down
19 changes: 19 additions & 0 deletions app/bundles/PointBundle/Config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@
'controller' => 'Mautic\PointBundle\Controller\Api\PointApiController::adjustPointsAction',
'method' => 'POST',
],
'mautic_api_pointgroupsstandard' => [
'standard_entity' => true,
'name' => 'pointGroups',
'path' => '/points/groups',
'controller' => Mautic\PointBundle\Controller\Api\PointGroupsApiController::class,
],
'mautic_api_getcontactpointgroups' => [
'path' => '/contacts/{contactId}/points/groups',
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::getContactPointGroupsAction',
],
'mautic_api_getcontactpointgroup' => [
'path' => '/contacts/{contactId}/points/groups/{groupId}',
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::getContactPointGroupAction',
],
'mautic_api_adjustcontactgrouppoints' => [
'path' => '/contacts/{contactId}/points/groups/{groupId}/{operator}/{value}',
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::adjustGroupPointsAction',
'method' => 'POST',
],
],
],

Expand Down
152 changes: 152 additions & 0 deletions app/bundles/PointBundle/Controller/Api/PointGroupsApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Mautic\PointBundle\Controller\Api;

use Doctrine\Persistence\ManagerRegistry;
use Mautic\ApiBundle\Controller\CommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\CoreBundle\Factory\MauticFactory;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Model\PointGroupModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;

/**
* @extends CommonApiController<Group>
*/
class PointGroupsApiController extends CommonApiController
{
/**
* @var PointGroupModel
*/
protected $model;

/** @phpstan-ignore-next-line the parent class uses the deprecated MauticFactory */
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper, MauticFactory $factory, PointGroupModel $pointGroupModel, private LeadModel $leadModel)
{
$this->model = $pointGroupModel;
$this->entityClass = Group::class;
$this->entityNameOne = 'pointGroup';
$this->entityNameMulti = 'pointGroups';
$this->serializerGroups = ['pointGroupDetails', 'pointGroupList', 'publishDetails'];

parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper, $factory);
}

public function getContactPointGroupsAction(int $contactId): Response
{
$contact = $this->leadModel->getEntity($contactId);

if (null === $contact) {
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
}

if (!$this->checkEntityAccess($contact)) {
return $this->accessDenied();
}

$groupScores = $contact->getGroupScores();
$view = $this->view(
[
'total' => count($groupScores),
'groupScores' => $groupScores,
],
Response::HTTP_OK
);

$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
$view->setContext($context);

return $this->handleView($view);
}

public function getContactPointGroupAction(int $contactId, int $groupId): Response
{
$contact = $this->leadModel->getEntity($contactId);

if (null === $contact) {
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
}

if (!$this->checkEntityAccess($contact)) {
return $this->accessDenied();
}

$pointGroup = $this->model->getEntity($groupId);
if (null === $pointGroup) {
return $this->notFound($this->translator->trans('mautic.lead.event.api.point.group.not.found'));
}

$groupScore = $contact->getGroupScore($pointGroup);
$view = $this->view(
[
'groupScore' => $groupScore,
],
Response::HTTP_OK
);

$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
$view->setContext($context);

return $this->handleView($view);
}

public function adjustGroupPointsAction(Request $request, IpLookupHelper $ipLookupHelper, int $contactId, int $groupId, string $operator, int $value): Response
{
$contact = $this->leadModel->getEntity($contactId);

if (null === $contact) {
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
}

if (!$this->checkEntityAccess($contact)) {
return $this->accessDenied();
}

$pointGroup = $this->model->getEntity($groupId);
if (null === $pointGroup) {
return $this->notFound($this->translator->trans('mautic.lead.event.api.point.group.not.found'));
}

if (!PointGroupModel::isAllowedPointOperation($operator)) {
return $this->badRequest($this->translator->trans('mautic.lead.event.api.operation.not.allowed'));
}

$oldScore = $contact->getGroupScore($pointGroup)?->getScore();
$contact = $this->model->adjustPoints($contact, $pointGroup, $value, $operator);
$newScore = $contact->getGroupScore($pointGroup)->getScore();
$delta = $newScore - ($oldScore ?? 0);

$eventName = InputHelper::clean($request->request->get('eventName', $this->translator->trans('mautic.point.event.manual_change')));
$actionName = InputHelper::clean($request->request->get('actionName', $this->translator->trans('mautic.lead.event.api')));
$contact->addPointsChangeLogEntry(
type: 'API',
name: $eventName,
action: $actionName,
pointChanges: $delta,
ip: $ipLookupHelper->getIpAddress(),
group: $pointGroup
);
$this->leadModel->saveEntity($contact, false);

$view = $this->view(['groupScore' => $contact->getGroupScore($pointGroup)], Response::HTTP_OK);
$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
$view->setContext($context);

return $this->handleView($view);
}
}
21 changes: 21 additions & 0 deletions app/bundles/PointBundle/Entity/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Mautic\PointBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Symfony\Component\Validator\Constraints as Assert;
Expand Down Expand Up @@ -38,6 +39,26 @@ public static function loadValidatorMetadata(ClassMetadata $metadata): void
]));
}

public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('pointGroup')
->addListProperties(
[
'id',
'name',
'description',
]
)
->addProperties(
[
'id',
'name',
'description',
]
)
->build();
}

public function getId(): ?int
{
return $this->id;
Expand Down
19 changes: 19 additions & 0 deletions app/bundles/PointBundle/Entity/GroupContactScore.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CommonEntity;
use Mautic\LeadBundle\Entity\Lead;
Expand Down Expand Up @@ -48,6 +49,24 @@ public static function loadMetadata(ORM\ClassMetadata $metadata): void
->build();
}

public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('groupContactScore')
->addListProperties(
[
'score',
'group',
]
)
->addProperties(
[
'score',
'group',
]
)
->build();
}

public function getContact(): Lead
{
return $this->contact;
Expand Down
8 changes: 8 additions & 0 deletions app/bundles/PointBundle/Entity/GroupRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public function getTableAlias(): string
{
return 'pl';
}

public function getEntities(array $args = [])
{
// Without qb it returns entities indexed by id instead of array indexes
$args['qb'] = $this->createQueryBuilder($this->getTableAlias());

return parent::getEntities($args);
}
}
5 changes: 5 additions & 0 deletions app/bundles/PointBundle/Model/PointGroupModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,9 @@ public function adjustPoints(Lead $contact, Group $group, int $points, string $o

return $contact;
}

public static function isAllowedPointOperation(string $operator): bool
{
return in_array($operator, [Lead::POINTS_ADD, Lead::POINTS_SUBTRACT, Lead::POINTS_MULTIPLY, Lead::POINTS_DIVIDE, Lead::POINTS_SET]);
}
}

0 comments on commit f86c50e

Please sign in to comment.