Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Project reward claims stock #39

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions src/ApiResource/Project/RewardApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use ApiPlatform\Metadata as API;
use App\Entity\Money;
use App\Entity\Project\Reward;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use App\State\Project\RewardStateProcessor;
use AutoMapper\Attribute\MapTo;
use Symfony\Component\Validator\Constraints as Assert;

Expand All @@ -18,7 +18,7 @@
shortName: 'ProjectReward',
stateOptions: new Options(entityClass: Reward::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class
processor: RewardStateProcessor::class
)]
class RewardApiResource
{
Expand Down
4 changes: 2 additions & 2 deletions src/ApiResource/Project/RewardClaimApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use ApiPlatform\Metadata as API;
use App\ApiResource\User\UserApiResource;
use App\Entity\Project\RewardClaim;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use App\State\Project\RewardClaimStateProcessor;
use Symfony\Component\Validator\Constraints as Assert;

/**
Expand All @@ -17,7 +17,7 @@
shortName: 'ProjectRewardClaim',
stateOptions: new Options(entityClass: RewardClaim::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class
processor: RewardClaimStateProcessor::class
)]
class RewardClaimApiResource
{
Expand Down
3 changes: 3 additions & 0 deletions src/Entity/Project/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
use App\Entity\Trait\TimestampedCreationEntity;
use App\Entity\Trait\TimestampedUpdationEntity;
use App\Entity\User\User;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\ProjectRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
class Project implements UserOwnedInterface, AccountingOwnerInterface
{
Expand Down
3 changes: 3 additions & 0 deletions src/Entity/Project/Reward.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace App\Entity\Project;

use App\Entity\Money;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\RewardRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
Expand All @@ -12,6 +14,7 @@
/**
* A ProjectReward is something the Project owner wishes to give in exchange for contributions to their Project.
*/
#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: RewardRepository::class)]
class Reward
{
Expand Down
3 changes: 3 additions & 0 deletions src/Entity/Project/RewardClaim.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use App\Entity\Interface\UserOwnedInterface;
use App\Entity\User\User;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\RewardClaimRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\ORM\Mapping as ORM;

#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: RewardClaimRepository::class)]
class RewardClaim implements UserOwnedInterface
{
Expand Down
9 changes: 6 additions & 3 deletions src/Entity/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use App\Entity\Trait\MigratedEntity;
use App\Entity\Trait\TimestampedCreationEntity;
use App\Entity\Trait\TimestampedUpdationEntity;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\User\UserRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
Expand All @@ -24,10 +26,11 @@
* This allows to keep an User's "wallet", withholding their non-raised fundings into their Accounting.
*/
#[Gedmo\Loggable()]
#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')]
#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')]
#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Index(fields: ['migratedId'])]
#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')]
#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')]
class User implements UserInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface
{
use MigratedEntity;
Expand Down Expand Up @@ -68,7 +71,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
/**
* The projects owned by this User.
*/
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class)]
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class, cascade: ['persist'])]
private Collection $projects;

/**
Expand Down
28 changes: 28 additions & 0 deletions src/Mapping/Provider/EntityMapProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Mapping\Provider;

use AutoMapper\Provider\ProviderInterface;
use Doctrine\ORM\EntityManagerInterface;

class EntityMapProvider implements ProviderInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
) {}

public function provide(string $targetType, mixed $source, array $context): object|array|null
{
if (!isset($source->id)) {
return null;
}

$repository = $this->entityManager->getRepository($targetType);

if (!$repository) {
throw new \Exception(\sprintf("No repository found for '%s' class. Is it an Entity?", $targetType));
}

return $repository->find($source->id);
}
}
17 changes: 17 additions & 0 deletions src/Service/Auth/AuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use App\Entity\User\User;
use App\Entity\User\UserToken;
use App\Repository\User\UserRepository;
use Symfony\Bundle\SecurityBundle\Security;

class AuthService
{
Expand All @@ -13,6 +15,8 @@ class AuthService

public function __construct(
private string $appSecret,
private Security $security,
private UserRepository $userRepository,
) {}

/**
Expand Down Expand Up @@ -40,4 +44,17 @@ public function generateUserToken(User $user, AuthTokenType $type): UserToken

return $token;
}

public function getUser(): ?User
{
$loggedInUser = $this->security->getUser();

if (!$loggedInUser) {
return null;
}

return $this->userRepository->findOneBy(
['username' => $loggedInUser->getUserIdentifier()]
);
}
}
34 changes: 34 additions & 0 deletions src/Service/Project/RewardService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Service\Project;

use App\Entity\Project\Reward;
use App\Entity\Project\RewardClaim;

class RewardService
{
/**
* Processes a RewardClaim for the set Reward.
*
* @return RewardClaim The processed RewardClaim with updated Reward
*/
public function processClaim(RewardClaim $claim): RewardClaim
{
$reward = $claim->getReward();

if (!$reward->hasUnits()) {
return $claim;
}

$available = $reward->getUnitsAvailable();

if ($available < 1) {
throw new \Exception('The claimed Reward has no units available');
}

$reward->addClaim($claim);
$reward->setUnitsAvailable($available - 1);

return $claim->setReward($reward);
}
}
2 changes: 1 addition & 1 deletion src/State/ApiResourceStateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
public function getEntity(mixed $data, Options $options): object
{
/** @var object */
$entity = $this->autoMapper->map($data, $options->getEntityClass(), ['skip_null_values' => true]);
$entity = $this->autoMapper->map($data, $options->getEntityClass());

return $entity;
}
Expand Down
32 changes: 32 additions & 0 deletions src/State/EntityStateProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\State;

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Doctrine\Common\State\RemoveProcessor;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class EntityStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: RemoveProcessor::class)]
private ProcessorInterface $deleteProcessor,
#[Autowire(service: PersistProcessor::class)]
private ProcessorInterface $persistProcessor,
) {}

/**
* @return T2
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
if ($operation instanceof DeleteOperationInterface) {
return $this->deleteProcessor->process($data, $operation, $uriVariables, $context);
}

return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}
36 changes: 13 additions & 23 deletions src/State/Project/ProjectStateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,21 @@

namespace App\State\Project;

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Doctrine\Common\State\RemoveProcessor;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\Project\ProjectApiResource;
use App\Entity\Project\Project;
use App\Mapping\AutoMapper;
use App\Repository\User\UserRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use App\Service\Auth\AuthService;
use App\State\EntityStateProcessor;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class ProjectStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: RemoveProcessor::class)]
private ProcessorInterface $deleteProcessor,
#[Autowire(service: PersistProcessor::class)]
private ProcessorInterface $persistProcessor,
private EntityStateProcessor $entityStateProcessor,
private AutoMapper $autoMapper,
private Security $security,
private UserRepository $userRepository,
private AuthService $authService,
) {}

/**
Expand All @@ -34,22 +27,19 @@ public function __construct(
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
/** @var Project */
$project = $this->autoMapper->map($data, Project::class, ['skip_null_values' => true]);
$project = $this->autoMapper->map($data, Project::class);

if (!isset($data->id)) {
$user = $this->security->getUser();
$owner = $this->userRepository->findOneBy(['username' => $user->getUserIdentifier()]);
if (!$project->getId()) {
$owner = $this->authService->getUser();

$project->setOwner($owner);
}
if (!$owner) {
throw new AuthenticationException();
}

if ($operation instanceof DeleteOperationInterface) {
$this->deleteProcessor->process($project, $operation, $uriVariables, $context);

return null;
$project->setOwner($owner);
}

$this->persistProcessor->process($project, $operation, $uriVariables, $context);
$project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context);

return $this->autoMapper->map($project, $data);
}
Expand Down
49 changes: 49 additions & 0 deletions src/State/Project/RewardClaimStateProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace App\State\Project;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\Project\RewardClaimApiResource;
use App\Entity\Project\RewardClaim;
use App\Mapping\AutoMapper;
use App\Service\Auth\AuthService;
use App\Service\Project\RewardService;
use App\State\EntityStateProcessor;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class RewardClaimStateProcessor implements ProcessorInterface
{
public function __construct(
private EntityStateProcessor $entityStateProcessor,
private AutoMapper $autoMapper,
private AuthService $authService,
private RewardService $rewardService,
) {}

/**
* @param RewardClaimApiResource $data
*
* @return RewardClaimApiResource|null
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
/** @var RewardClaim */
$claim = $this->autoMapper->map($data, RewardClaim::class);

if (!$claim->getId()) {
$owner = $this->authService->getUser();

if (!$owner) {
throw new AuthenticationException();
}

$claim->setOwner($owner);
}

$claim = $this->rewardService->processClaim($claim);
$claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context);

return $this->autoMapper->map($claim, $data);
}
}
Loading
Loading