diff --git a/config/services.yaml b/config/services.yaml index 29cddcce..6ab8b946 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -18,6 +18,8 @@ services: tags: ['app.lib.benzina.pump.pump'] App\Library\Economy\Currency\ExchangeInterface: tags: ['app.lib.economy.currency.exchange'] + App\Matchfunding\MatchStrategy\MatchStrategyInterface: + tags: ['app.matchfunding.strategy.strategy'] AutoMapper\Provider\ProviderInterface: tags: ['app.mapping.map_provider'] AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface: @@ -57,6 +59,10 @@ services: - '%env(STRIPE_API_KEY)%' - '%env(STRIPE_WEBHOOK_SECRET)%' + App\Matchfunding\MatchStrategy\MatchStrategyLocator: + arguments: + - !tagged 'app.matchfunding.strategy.strategy' + App\Service\Auth\AuthService: arguments: - '%env(APP_SECRET)%' diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php new file mode 100644 index 00000000..a5b688aa --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -0,0 +1,52 @@ +getProject(); case Tipjar::class: return $this->getTipjar(); + case MatchCall::class: + return $this->getMatchCall(); } return null; @@ -121,6 +127,8 @@ public function setOwner(?AccountingOwnerInterface $owner): static return $this->setProject($owner); case Tipjar::class: return $this->setTipjar($owner); + case MatchCall::class: + return $this->setMatchCall($owner); } return $this; @@ -191,4 +199,21 @@ public function setTipjar(?Tipjar $tipjar): static return $this; } + + public function getMatchCall(): ?MatchCall + { + return $this->matchCall; + } + + public function setMatchCall(MatchCall $matchCall): static + { + // set the owning side of the relation if necessary + if ($matchCall->getAccounting() !== $this) { + $matchCall->setAccounting($this); + } + + $this->matchCall = $matchCall; + + return $this; + } } diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php new file mode 100644 index 00000000..9545954d --- /dev/null +++ b/src/Entity/Matchfunding/MatchCall.php @@ -0,0 +1,129 @@ + + */ + #[ORM\ManyToMany(targetEntity: User::class)] + private Collection $managers; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'matchCall', targetEntity: MatchCallSubmission::class)] + private Collection $matchCallSubmissions; + + #[ORM\Column(length: 255)] + private ?string $strategyName = null; + + public function __construct() + { + $this->accounting = Accounting::of($this); + $this->managers = new ArrayCollection(); + $this->matchCallSubmissions = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getAccounting(): ?Accounting + { + return $this->accounting; + } + + public function setAccounting(Accounting $accounting): static + { + $this->accounting = $accounting; + + return $this; + } + + /** + * @return Collection + */ + public function getManagers(): Collection + { + return $this->managers; + } + + public function addManager(User $manager): static + { + if (!$this->managers->contains($manager)) { + $this->managers->add($manager); + } + + return $this; + } + + public function removeManager(User $manager): static + { + $this->managers->removeElement($manager); + + return $this; + } + + /** + * @return Collection + */ + public function getMatchCallSubmissions(): Collection + { + return $this->matchCallSubmissions; + } + + public function addMatchCallSubmission(MatchCallSubmission $submission): static + { + if (!$this->matchCallSubmissions->contains($submission)) { + $this->matchCallSubmissions->add($submission); + $submission->setMatchCall($this); + } + + return $this; + } + + public function removeMatchCallSubmission(MatchCallSubmission $submission): static + { + if ($this->matchCallSubmissions->removeElement($submission)) { + // set the owning side to null (unless already changed) + if ($submission->getMatchCall() === $this) { + $submission->setMatchCall(null); + } + } + + return $this; + } + + public function getStrategyName(): ?string + { + return $this->strategyName; + } + + public function setStrategyName(string $strategyName): static + { + $this->strategyName = $strategyName; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchCallSubmission.php b/src/Entity/Matchfunding/MatchCallSubmission.php new file mode 100644 index 00000000..88231197 --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallSubmission.php @@ -0,0 +1,68 @@ +id; + } + + public function getMatchCall(): ?MatchCall + { + return $this->matchCall; + } + + public function setMatchCall(?MatchCall $matchCall): static + { + $this->matchCall = $matchCall; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } + + public function getStatus(): ?MatchCallSubmissionStatus + { + return $this->status; + } + + public function setStatus(MatchCallSubmissionStatus $status): static + { + $this->status = $status; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php new file mode 100644 index 00000000..ce74d7cf --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php @@ -0,0 +1,21 @@ + + */ + #[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchCallSubmission::class)] + private Collection $matchCallSubmissions; + public function __construct() { $this->accounting = Accounting::of($this); $this->rewards = new ArrayCollection(); + $this->matchCallSubmissions = new ArrayCollection(); } public function getId(): ?int @@ -167,4 +175,34 @@ public function removeReward(Reward $reward): static return $this; } + + /** + * @return Collection + */ + public function getMatchCallSubmissions(): Collection + { + return $this->matchCallSubmissions; + } + + public function addMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static + { + if (!$this->matchCallSubmissions->contains($MatchCallSubmission)) { + $this->matchCallSubmissions->add($MatchCallSubmission); + $MatchCallSubmission->setProject($this); + } + + return $this; + } + + public function removeMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static + { + if ($this->matchCallSubmissions->removeElement($MatchCallSubmission)) { + // set the owning side to null (unless already changed) + if ($MatchCallSubmission->getProject() === $this) { + $MatchCallSubmission->setProject(null); + } + } + + return $this; + } } diff --git a/src/EventListener/MatchfundingTransactionsListener.php b/src/EventListener/MatchfundingTransactionsListener.php new file mode 100644 index 00000000..fd9d3f19 --- /dev/null +++ b/src/EventListener/MatchfundingTransactionsListener.php @@ -0,0 +1,56 @@ +getTarget()->getOwner(); + + if (!$target instanceof Project) { + return; + } + + $submissions = $target->getMatchCallSubmissions(); + + foreach ($submissions as $submission) { + if ($submission->getStatus() !== MatchCallSubmissionStatus::Accepted) { + continue; + } + + $call = $submission->getMatchCall(); + $strategy = $this->matchStrategyLocator->getForCall($call); + $match = $strategy->match($transaction); + + if ($match->getId() !== null) { + return; + } + + $event->getObjectManager()->persist($match); + $event->getObjectManager()->flush(); + } + } +} diff --git a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php new file mode 100644 index 00000000..c7582249 --- /dev/null +++ b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php @@ -0,0 +1,23 @@ + */ + private array $strategiesByName = []; + + public function __construct( + iterable $strategies, + ) { + foreach ($strategies as $strategy) { + $strategyName = $strategy::getName(); + + if (\array_key_exists($strategyName, $this->strategiesByName)) { + throw new MatchStrategyDuplicatedException( + $strategyName, + $strategy::class, + $this->strategiesByName[$strategyName]::class + ); + } + + $this->strategiesByName[$strategyName] = $strategy; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->strategiesByName; + } + + public function get(string $strategyName): ?MatchStrategyInterface + { + if (!array_key_exists($strategyName, $this->strategiesByName)) { + throw new MatchStrategyNotFoundException($strategyName); + } + + return $this->strategiesByName[$strategyName]; + } + + public function getForCall(MatchCall $call): ?MatchStrategyInterface + { + return $this->get($call->getStrategyName()); + } +} diff --git a/src/Repository/Matchfunding/MatchCallRepository.php b/src/Repository/Matchfunding/MatchCallRepository.php new file mode 100644 index 00000000..a7fb3cde --- /dev/null +++ b/src/Repository/Matchfunding/MatchCallRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchCallRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchCall::class); + } + + // /** + // * @return MatchCall[] Returns an array of MatchCall objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('m.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?MatchCall + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/Matchfunding/MatchCallSubmissionRepository.php b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php new file mode 100644 index 00000000..54b86466 --- /dev/null +++ b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchCallSubmissionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchCallSubmission::class); + } + + // /** + // * @return MatchCallSubmission[] Returns an array of MatchCallSubmission objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('m.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?MatchCallSubmission + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/State/Matchfunding/MatchStrategyStateProvider.php b/src/State/Matchfunding/MatchStrategyStateProvider.php new file mode 100644 index 00000000..338c2fb3 --- /dev/null +++ b/src/State/Matchfunding/MatchStrategyStateProvider.php @@ -0,0 +1,47 @@ +matchStrategyLocator->get($uriVariables['name']); + + return $this->getApiResource($strategy); + } catch (MatchStrategyNotFoundException $e) { + return null; + } + } + + $strategies = $this->matchStrategyLocator->getAll(); + + $resources = []; + foreach ($strategies as $strategy) { + $resources[] = $this->getApiResource($strategy); + } + + return $resources; + } + + private function getApiResource(MatchStrategyInterface $strategy): MatchStrategyApiResource + { + $resource = new MatchStrategyApiResource(); + $resource->name = $strategy::getName(); + + return $resource; + } +}