From d305ae5f523422e646e348ce53294f4044a567e5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Dec 2024 15:17:56 +0100 Subject: [PATCH 001/203] Add matchfunding strategies interface and locator --- config/services.yaml | 6 +++ .../MatchStrategy/MatchStrategyInterface.php | 22 +++++++++++ .../MatchStrategy/MatchStrategyLocator.php | 39 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/Matchfunding/MatchStrategy/MatchStrategyInterface.php create mode 100644 src/Matchfunding/MatchStrategy/MatchStrategyLocator.php diff --git a/config/services.yaml b/config/services.yaml index 654922d..9b1dcd9 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'] @@ -55,6 +57,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/Matchfunding/MatchStrategy/MatchStrategyInterface.php b/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php new file mode 100644 index 0000000..b08cda7 --- /dev/null +++ b/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php @@ -0,0 +1,22 @@ + */ + private array $strategiesByName; + + public function __construct( + iterable $strategies + ) { + $strategies = \iterator_to_array($strategies); + + foreach ($strategies as $strategy) { + $strategyName = $strategy::getName(); + + if (\array_key_exists($strategyName, $this->strategiesByName)) { + throw new \Exception(\sprintf( + "Duplicate strategy name '%s' by '%s', value already in use by '%s'", + $strategyName, + $strategy::class, + $this->strategiesByName[$strategyName]::class + )); + } + + $this->strategiesByName[$strategyName] = $strategy; + } + } + + public function get(string $strategyName): ?MatchStrategyInterface + { + if (!array_key_exists($strategyName, $this->strategiesByName)) { + return null; + } + + return $this->strategiesByName[$strategyName]; + } +} From 91ec009afa7b82fc0bf0b984974d9350d1cbde16 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Dec 2024 15:51:17 +0100 Subject: [PATCH 002/203] Add match call entity --- src/Entity/Accounting/Accounting.php | 25 ++++ src/Entity/Matchfunding/MatchCall.php | 129 ++++++++++++++++++ .../MatchStrategy/MatchStrategyLocator.php | 7 + .../Matchfunding/MatchCallRepository.php | 43 ++++++ 4 files changed, 204 insertions(+) create mode 100644 src/Entity/Matchfunding/MatchCall.php create mode 100644 src/Repository/Matchfunding/MatchCallRepository.php diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php index 241069d..a4fb6d0 100644 --- a/src/Entity/Accounting/Accounting.php +++ b/src/Entity/Accounting/Accounting.php @@ -3,6 +3,7 @@ namespace App\Entity\Accounting; use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Matchfunding\MatchCall; use App\Entity\Project\Project; use App\Entity\Tipjar; use App\Entity\User\User; @@ -43,6 +44,9 @@ class Accounting #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])] private ?Tipjar $tipjar = null; + #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])] + private ?MatchCall $matchCall = null; + /** * Create a new Accounting entity instance for the given owner. */ @@ -101,6 +105,8 @@ class Accounting return $this->getProject(); case Tipjar::class: return $this->getTipjar(); + case MatchCall::class: + return $this->getMatchCall(); } return null; @@ -121,6 +127,8 @@ class Accounting return $this->setProject($owner); case Tipjar::class: return $this->setTipjar($owner); + case MatchCall::class: + return $this->setMatchCall($owner); } return $this; @@ -191,4 +199,21 @@ class Accounting 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 0000000..dd770df --- /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: MatchSubmission::class)] + private Collection $matchSubmissions; + + #[ORM\Column(length: 255)] + private ?string $strategyName = null; + + public function __construct() + { + $this->accounting = Accounting::of($this); + $this->managers = new ArrayCollection(); + $this->matchSubmissions = 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 getMatchSubmissions(): Collection + { + return $this->matchSubmissions; + } + + public function addMatchSubmission(MatchSubmission $submission): static + { + if (!$this->matchSubmissions->contains($submission)) { + $this->matchSubmissions->add($submission); + $submission->setMatchCall($this); + } + + return $this; + } + + public function removeMatchSubmission(MatchSubmission $submission): static + { + if ($this->matchSubmissions->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/Matchfunding/MatchStrategy/MatchStrategyLocator.php b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php index 34a7d3b..bda41f9 100644 --- a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php +++ b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php @@ -2,6 +2,8 @@ namespace App\Matchfunding\MatchStrategy; +use App\Entity\Matchfunding\MatchCall; + class MatchStrategyLocator { /** @var array */ @@ -36,4 +38,9 @@ class MatchStrategyLocator 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 0000000..15534cf --- /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() +// ; +// } +} From 0d5d1c5df9ad0098e46110472ac94e1186131488 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Dec 2024 15:52:21 +0100 Subject: [PATCH 003/203] Add match submission entity --- src/Entity/Matchfunding/MatchSubmission.php | 68 +++++++++++++++++++ .../Matchfunding/MatchSubmissionStatus.php | 21 ++++++ src/Entity/Project/Project.php | 38 +++++++++++ .../MatchSubmissionRepository.php | 43 ++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 src/Entity/Matchfunding/MatchSubmission.php create mode 100644 src/Entity/Matchfunding/MatchSubmissionStatus.php create mode 100644 src/Repository/Matchfunding/MatchSubmissionRepository.php diff --git a/src/Entity/Matchfunding/MatchSubmission.php b/src/Entity/Matchfunding/MatchSubmission.php new file mode 100644 index 0000000..2a913b2 --- /dev/null +++ b/src/Entity/Matchfunding/MatchSubmission.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(): ?MatchSubmissionStatus + { + return $this->status; + } + + public function setStatus(MatchSubmissionStatus $status): static + { + $this->status = $status; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchSubmissionStatus.php b/src/Entity/Matchfunding/MatchSubmissionStatus.php new file mode 100644 index 0000000..73dc03b --- /dev/null +++ b/src/Entity/Matchfunding/MatchSubmissionStatus.php @@ -0,0 +1,21 @@ + + */ + #[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchSubmission::class)] + private Collection $matchSubmissions; + public function __construct() { $this->accounting = Accounting::of($this); $this->rewards = new ArrayCollection(); + $this->matchSubmissions = new ArrayCollection(); } public function getId(): ?int @@ -159,4 +167,34 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface return $this; } + + /** + * @return Collection + */ + public function getMatchSubmissions(): Collection + { + return $this->matchSubmissions; + } + + public function addMatchSubmission(MatchSubmission $matchSubmission): static + { + if (!$this->matchSubmissions->contains($matchSubmission)) { + $this->matchSubmissions->add($matchSubmission); + $matchSubmission->setProject($this); + } + + return $this; + } + + public function removeMatchSubmission(MatchSubmission $matchSubmission): static + { + if ($this->matchSubmissions->removeElement($matchSubmission)) { + // set the owning side to null (unless already changed) + if ($matchSubmission->getProject() === $this) { + $matchSubmission->setProject(null); + } + } + + return $this; + } } diff --git a/src/Repository/Matchfunding/MatchSubmissionRepository.php b/src/Repository/Matchfunding/MatchSubmissionRepository.php new file mode 100644 index 0000000..35eb4f4 --- /dev/null +++ b/src/Repository/Matchfunding/MatchSubmissionRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchSubmissionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchSubmission::class); + } + + // /** + // * @return MatchSubmission[] Returns an array of MatchSubmission 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): ?MatchSubmission + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} From 54b45b38c6f9721eb70acfa096871d95a393e92c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Dec 2024 15:57:10 +0100 Subject: [PATCH 004/203] Update match strategy locator error handling --- src/Matchfunding/MatchStrategy/MatchStrategyLocator.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php index bda41f9..71ae51b 100644 --- a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php +++ b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php @@ -19,7 +19,7 @@ class MatchStrategyLocator if (\array_key_exists($strategyName, $this->strategiesByName)) { throw new \Exception(\sprintf( - "Duplicate strategy name '%s' by '%s', value already in use by '%s'", + "Duplicate MatchStrategy name '%s' by '%s', value already in use by '%s'", $strategyName, $strategy::class, $this->strategiesByName[$strategyName]::class @@ -33,7 +33,10 @@ class MatchStrategyLocator public function get(string $strategyName): ?MatchStrategyInterface { if (!array_key_exists($strategyName, $this->strategiesByName)) { - return null; + throw new \Exception(\sprintf( + "Could not find a MatchStrategy by the name '%s', value does not exist", + $strategyName + )); } return $this->strategiesByName[$strategyName]; From ae098d7c649b26f5d5641d0327ecc8bbb56dc903 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 09:58:46 +0100 Subject: [PATCH 005/203] Add matchfunding transactions listener --- .../MatchfundingTransactionsListener.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/EventListener/MatchfundingTransactionsListener.php diff --git a/src/EventListener/MatchfundingTransactionsListener.php b/src/EventListener/MatchfundingTransactionsListener.php new file mode 100644 index 0000000..0334119 --- /dev/null +++ b/src/EventListener/MatchfundingTransactionsListener.php @@ -0,0 +1,56 @@ +getTarget()->getOwner(); + + if (!$target instanceof Project) { + return; + } + + $submissions = $target->getMatchSubmissions(); + + foreach ($submissions as $submission) { + if ($submission->getStatus() !== MatchSubmissionStatus::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(); + } + } +} From cf9d051e168fad95fba9efef4143b62658786467 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 10:07:44 +0100 Subject: [PATCH 006/203] Fixes in code style --- .../Matchfunding/MatchSubmissionStatus.php | 4 +- .../MatchfundingTransactionsListener.php | 4 +- .../MatchStrategy/MatchStrategyInterface.php | 6 +-- .../MatchStrategy/MatchStrategyLocator.php | 2 +- .../Matchfunding/MatchCallRepository.php | 46 +++++++++---------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Entity/Matchfunding/MatchSubmissionStatus.php b/src/Entity/Matchfunding/MatchSubmissionStatus.php index 73dc03b..b26e245 100644 --- a/src/Entity/Matchfunding/MatchSubmissionStatus.php +++ b/src/Entity/Matchfunding/MatchSubmissionStatus.php @@ -10,12 +10,12 @@ enum MatchSubmissionStatus: string case InReview = 'in_review'; /** - * The MatchSubmission was accepted into the MatchCall + * The MatchSubmission was accepted into the MatchCall. */ case Accepted = 'accepted'; /** - * The MatchSubmission was rejected out of the MatchCall + * The MatchSubmission was rejected out of the MatchCall. */ case Rejected = 'rejected'; } diff --git a/src/EventListener/MatchfundingTransactionsListener.php b/src/EventListener/MatchfundingTransactionsListener.php index 0334119..a9c2570 100644 --- a/src/EventListener/MatchfundingTransactionsListener.php +++ b/src/EventListener/MatchfundingTransactionsListener.php @@ -18,11 +18,11 @@ use Doctrine\ORM\Events; final class MatchfundingTransactionsListener { public function __construct( - private MatchStrategyLocator $matchStrategyLocator + private MatchStrategyLocator $matchStrategyLocator, ) {} /** - * Generates an income statement for User-received Transactions. + * Generates matched Transactions for Transactions inside a MatchCall. */ public function processTransaction( Transaction $transaction, diff --git a/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php b/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php index b08cda7..f373224 100644 --- a/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php +++ b/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php @@ -12,10 +12,10 @@ interface MatchStrategyInterface public static function getName(): string; /** - * Process a Transaction targeting a Matchfunding that uses this strategy - * + * Process a Transaction targeting a Matchfunding that uses this strategy. + * * @param Transaction $transaction The input Transaction that is eligible for matching - * + * * @return Transaction A new output Transaction for the matching */ public function match(Transaction $transaction): Transaction; diff --git a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php index 71ae51b..d353e0c 100644 --- a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php +++ b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php @@ -10,7 +10,7 @@ class MatchStrategyLocator private array $strategiesByName; public function __construct( - iterable $strategies + iterable $strategies, ) { $strategies = \iterator_to_array($strategies); diff --git a/src/Repository/Matchfunding/MatchCallRepository.php b/src/Repository/Matchfunding/MatchCallRepository.php index 15534cf..a7fb3cd 100644 --- a/src/Repository/Matchfunding/MatchCallRepository.php +++ b/src/Repository/Matchfunding/MatchCallRepository.php @@ -16,28 +16,28 @@ class MatchCallRepository extends ServiceEntityRepository 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() -// ; -// } + // /** + // * @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() -// ; -// } + // public function findOneBySomeField($value): ?MatchCall + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } } From a8370adbec0b8051e24096f87a071ebaa7f209db Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 10:38:26 +0100 Subject: [PATCH 007/203] Start match call api resource --- .../Matchfunding/MatchCallApiResource.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/ApiResource/Matchfunding/MatchCallApiResource.php diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php new file mode 100644 index 0000000..487e282 --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -0,0 +1,45 @@ + Date: Thu, 19 Dec 2024 12:00:16 +0100 Subject: [PATCH 008/203] Add match call submission api resource --- .../Matchfunding/MatchCallApiResource.php | 7 +++ .../MatchSubmissionApiResource.php | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/ApiResource/Matchfunding/MatchSubmissionApiResource.php diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 487e282..cd63eb1 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -42,4 +42,11 @@ class MatchCallApiResource */ #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALL_EDIT", object)')] public array $managers; + + /** + * A list of the MatchSubmissions received by this MatchCall. + * + * @var MatchSubmissionApiResource[] + */ + public array $matchSubmissions; } diff --git a/src/ApiResource/Matchfunding/MatchSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchSubmissionApiResource.php new file mode 100644 index 0000000..b9a9327 --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchSubmissionApiResource.php @@ -0,0 +1,43 @@ + Date: Thu, 19 Dec 2024 12:37:30 +0100 Subject: [PATCH 009/203] Throw custom exceptions for match strategies --- .../MatchStrategyDuplicatedException.php | 23 +++++++++++++++++++ .../MatchStrategyNotFoundException.php | 19 +++++++++++++++ .../MatchStrategy/MatchStrategyLocator.php | 16 +++++-------- 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php create mode 100644 src/Matchfunding/MatchStrategy/Exception/MatchStrategyNotFoundException.php diff --git a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php new file mode 100644 index 0000000..c758224 --- /dev/null +++ b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php @@ -0,0 +1,23 @@ + */ - private array $strategiesByName; + private array $strategiesByName = []; public function __construct( iterable $strategies, ) { - $strategies = \iterator_to_array($strategies); - foreach ($strategies as $strategy) { $strategyName = $strategy::getName(); if (\array_key_exists($strategyName, $this->strategiesByName)) { - throw new \Exception(\sprintf( - "Duplicate MatchStrategy name '%s' by '%s', value already in use by '%s'", + throw new MatchStrategyDuplicatedException( $strategyName, $strategy::class, $this->strategiesByName[$strategyName]::class - )); + ); } $this->strategiesByName[$strategyName] = $strategy; @@ -33,10 +32,7 @@ class MatchStrategyLocator public function get(string $strategyName): ?MatchStrategyInterface { if (!array_key_exists($strategyName, $this->strategiesByName)) { - throw new \Exception(\sprintf( - "Could not find a MatchStrategy by the name '%s', value does not exist", - $strategyName - )); + throw new MatchStrategyNotFoundException($strategyName); } return $this->strategiesByName[$strategyName]; From edc3a63f479ce4048026d15ed65d96c7d15a4032 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 12:38:14 +0100 Subject: [PATCH 010/203] Return all match strategies --- src/Matchfunding/MatchStrategy/MatchStrategyLocator.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php index 1c063e1..5fb5aa7 100644 --- a/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php +++ b/src/Matchfunding/MatchStrategy/MatchStrategyLocator.php @@ -29,6 +29,14 @@ class MatchStrategyLocator } } + /** + * @return array + */ + public function getAll(): array + { + return $this->strategiesByName; + } + public function get(string $strategyName): ?MatchStrategyInterface { if (!array_key_exists($strategyName, $this->strategiesByName)) { From 71432d9b2127534c2e918217dec15ff8a3afe3cb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 12:43:17 +0100 Subject: [PATCH 011/203] Add match strategy api resource --- .../Matchfunding/MatchStrategyApiResource.php | 20 ++++++++ .../MatchStrategyStateProvider.php | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/ApiResource/Matchfunding/MatchStrategyApiResource.php create mode 100644 src/State/Matchfunding/MatchStrategyStateProvider.php diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php new file mode 100644 index 0000000..6e9399a --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -0,0 +1,20 @@ +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; + } +} From 9bf92d624579a3988bacd0cb67bb1f176fc5d020 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 19 Dec 2024 12:52:04 +0100 Subject: [PATCH 012/203] Rename MatchSubmission to MatchCallSubmission --- .../Matchfunding/MatchCallApiResource.php | 10 +++--- ...php => MatchCallSubmissionApiResource.php} | 20 ++++++------ src/Entity/Matchfunding/MatchCall.php | 24 +++++++------- ...Submission.php => MatchCallSubmission.php} | 18 +++++------ .../MatchCallSubmissionStatus.php | 21 ++++++++++++ .../Matchfunding/MatchSubmissionStatus.php | 21 ------------ src/Entity/Project/Project.php | 32 +++++++++---------- .../MatchfundingTransactionsListener.php | 6 ++-- ....php => MatchCallSubmissionRepository.php} | 12 +++---- 9 files changed, 82 insertions(+), 82 deletions(-) rename src/ApiResource/Matchfunding/{MatchSubmissionApiResource.php => MatchCallSubmissionApiResource.php} (56%) rename src/Entity/Matchfunding/{MatchSubmission.php => MatchCallSubmission.php} (65%) create mode 100644 src/Entity/Matchfunding/MatchCallSubmissionStatus.php delete mode 100644 src/Entity/Matchfunding/MatchSubmissionStatus.php rename src/Repository/Matchfunding/{MatchSubmissionRepository.php => MatchCallSubmissionRepository.php} (69%) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index cd63eb1..a5b688a 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -11,11 +11,11 @@ use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; /** - * A MatchCall is an owned and managed event which accepts MatchSubmissions from Projects to receive *matchfunding* financement. + * A MatchCall is an owned and managed event which accepts MatchCallSubmissions from Projects to receive *matchfunding* financement. * This means any money inside a Transaction going to a Project in a MatchCall will be matched with funds from the MatchCall accounting. * \ * \ - * MatchSubmissions from Projects can be accepted or rejected by the managers. + * MatchCallSubmissions from Projects can be accepted or rejected by the managers. * They can also choose from predefined strategies and tune them to perform the matching. */ #[API\ApiResource( @@ -44,9 +44,9 @@ class MatchCallApiResource public array $managers; /** - * A list of the MatchSubmissions received by this MatchCall. + * A list of the MatchCallSubmissions received by this MatchCall. * - * @var MatchSubmissionApiResource[] + * @var MatchCallSubmissionApiResource[] */ - public array $matchSubmissions; + public array $matchCallSubmissions; } diff --git a/src/ApiResource/Matchfunding/MatchSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php similarity index 56% rename from src/ApiResource/Matchfunding/MatchSubmissionApiResource.php rename to src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index b9a9327..3a6f8c8 100644 --- a/src/ApiResource/Matchfunding/MatchSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -5,27 +5,27 @@ namespace App\ApiResource\Matchfunding; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Project\ProjectApiResource; -use App\Entity\Matchfunding\MatchSubmission; -use App\Entity\Matchfunding\MatchSubmissionStatus; +use App\Entity\Matchfunding\MatchCallSubmission; +use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; /** - * MatchSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement. + * MatchCallSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement. */ #[API\ApiResource( - shortName: 'MatchSubmission', - stateOptions: new Options(entityClass: MatchSubmission::class), + shortName: 'MatchCallSubmission', + stateOptions: new Options(entityClass: MatchCallSubmission::class), provider: ApiResourceStateProvider::class, processor: ApiResourceStateProcessor::class )] -class MatchSubmissionApiResource +class MatchCallSubmissionApiResource { #[API\ApiProperty(identifier: true, writable: false)] public int $id; /** - * The MatchCall to which this MatchSubmission belongs to. + * The MatchCall to which this MatchCallSubmission belongs to. */ public MatchCallApiResource $matchCall; @@ -36,8 +36,8 @@ class MatchSubmissionApiResource /** * The status of the Project's application for the MatchCall.\ - * Only MatchSubmissions with an status `accepted` will receive matchfunding. + * Only MatchCallSubmissions with an status `accepted` will receive matchfunding. */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHSUBMISSION_EDIT", object)')] - public MatchSubmissionStatus $status; + #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALLSUBMISSION_EDIT", object)')] + public MatchCallSubmissionStatus $status; } diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index dd770df..9545954 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -29,10 +29,10 @@ class MatchCall implements AccountingOwnerInterface private Collection $managers; /** - * @var Collection + * @var Collection */ - #[ORM\OneToMany(mappedBy: 'matchCall', targetEntity: MatchSubmission::class)] - private Collection $matchSubmissions; + #[ORM\OneToMany(mappedBy: 'matchCall', targetEntity: MatchCallSubmission::class)] + private Collection $matchCallSubmissions; #[ORM\Column(length: 255)] private ?string $strategyName = null; @@ -41,7 +41,7 @@ class MatchCall implements AccountingOwnerInterface { $this->accounting = Accounting::of($this); $this->managers = new ArrayCollection(); - $this->matchSubmissions = new ArrayCollection(); + $this->matchCallSubmissions = new ArrayCollection(); } public function getId(): ?int @@ -86,26 +86,26 @@ class MatchCall implements AccountingOwnerInterface } /** - * @return Collection + * @return Collection */ - public function getMatchSubmissions(): Collection + public function getMatchCallSubmissions(): Collection { - return $this->matchSubmissions; + return $this->matchCallSubmissions; } - public function addMatchSubmission(MatchSubmission $submission): static + public function addMatchCallSubmission(MatchCallSubmission $submission): static { - if (!$this->matchSubmissions->contains($submission)) { - $this->matchSubmissions->add($submission); + if (!$this->matchCallSubmissions->contains($submission)) { + $this->matchCallSubmissions->add($submission); $submission->setMatchCall($this); } return $this; } - public function removeMatchSubmission(MatchSubmission $submission): static + public function removeMatchCallSubmission(MatchCallSubmission $submission): static { - if ($this->matchSubmissions->removeElement($submission)) { + if ($this->matchCallSubmissions->removeElement($submission)) { // set the owning side to null (unless already changed) if ($submission->getMatchCall() === $this) { $submission->setMatchCall(null); diff --git a/src/Entity/Matchfunding/MatchSubmission.php b/src/Entity/Matchfunding/MatchCallSubmission.php similarity index 65% rename from src/Entity/Matchfunding/MatchSubmission.php rename to src/Entity/Matchfunding/MatchCallSubmission.php index 2a913b2..8823119 100644 --- a/src/Entity/Matchfunding/MatchSubmission.php +++ b/src/Entity/Matchfunding/MatchCallSubmission.php @@ -3,27 +3,27 @@ namespace App\Entity\Matchfunding; use App\Entity\Project\Project; -use App\Repository\Matchfunding\MatchSubmissionRepository; +use App\Repository\Matchfunding\MatchCallSubmissionRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: MatchSubmissionRepository::class)] -class MatchSubmission +#[ORM\Entity(repositoryClass: MatchCallSubmissionRepository::class)] +class MatchCallSubmission { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; - #[ORM\ManyToOne(inversedBy: 'matchSubmissions')] + #[ORM\ManyToOne(inversedBy: 'matchCallSubmissions')] #[ORM\JoinColumn(nullable: false)] private ?MatchCall $matchCall = null; - #[ORM\ManyToOne(inversedBy: 'matchSubmissions')] + #[ORM\ManyToOne(inversedBy: 'matchCallSubmissions')] #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; - #[ORM\Column(enumType: MatchSubmissionStatus::class)] - private ?MatchSubmissionStatus $status = null; + #[ORM\Column(enumType: MatchCallSubmissionStatus::class)] + private ?MatchCallSubmissionStatus $status = null; public function getId(): ?int { @@ -54,12 +54,12 @@ class MatchSubmission return $this; } - public function getStatus(): ?MatchSubmissionStatus + public function getStatus(): ?MatchCallSubmissionStatus { return $this->status; } - public function setStatus(MatchSubmissionStatus $status): static + public function setStatus(MatchCallSubmissionStatus $status): static { $this->status = $status; diff --git a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php new file mode 100644 index 0000000..ce74d7c --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php @@ -0,0 +1,21 @@ + + * @var Collection */ - #[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchSubmission::class)] - private Collection $matchSubmissions; + #[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchCallSubmission::class)] + private Collection $matchCallSubmissions; public function __construct() { $this->accounting = Accounting::of($this); $this->rewards = new ArrayCollection(); - $this->matchSubmissions = new ArrayCollection(); + $this->matchCallSubmissions = new ArrayCollection(); } public function getId(): ?int @@ -169,29 +169,29 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface } /** - * @return Collection + * @return Collection */ - public function getMatchSubmissions(): Collection + public function getMatchCallSubmissions(): Collection { - return $this->matchSubmissions; + return $this->matchCallSubmissions; } - public function addMatchSubmission(MatchSubmission $matchSubmission): static + public function addMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static { - if (!$this->matchSubmissions->contains($matchSubmission)) { - $this->matchSubmissions->add($matchSubmission); - $matchSubmission->setProject($this); + if (!$this->matchCallSubmissions->contains($MatchCallSubmission)) { + $this->matchCallSubmissions->add($MatchCallSubmission); + $MatchCallSubmission->setProject($this); } return $this; } - public function removeMatchSubmission(MatchSubmission $matchSubmission): static + public function removeMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static { - if ($this->matchSubmissions->removeElement($matchSubmission)) { + if ($this->matchCallSubmissions->removeElement($MatchCallSubmission)) { // set the owning side to null (unless already changed) - if ($matchSubmission->getProject() === $this) { - $matchSubmission->setProject(null); + if ($MatchCallSubmission->getProject() === $this) { + $MatchCallSubmission->setProject(null); } } diff --git a/src/EventListener/MatchfundingTransactionsListener.php b/src/EventListener/MatchfundingTransactionsListener.php index a9c2570..fd9d3f1 100644 --- a/src/EventListener/MatchfundingTransactionsListener.php +++ b/src/EventListener/MatchfundingTransactionsListener.php @@ -3,7 +3,7 @@ namespace App\EventListener; use App\Entity\Accounting\Transaction; -use App\Entity\Matchfunding\MatchSubmissionStatus; +use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\Entity\Project\Project; use App\Matchfunding\MatchStrategy\MatchStrategyLocator; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; @@ -34,10 +34,10 @@ final class MatchfundingTransactionsListener return; } - $submissions = $target->getMatchSubmissions(); + $submissions = $target->getMatchCallSubmissions(); foreach ($submissions as $submission) { - if ($submission->getStatus() !== MatchSubmissionStatus::Accepted) { + if ($submission->getStatus() !== MatchCallSubmissionStatus::Accepted) { continue; } diff --git a/src/Repository/Matchfunding/MatchSubmissionRepository.php b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php similarity index 69% rename from src/Repository/Matchfunding/MatchSubmissionRepository.php rename to src/Repository/Matchfunding/MatchCallSubmissionRepository.php index 35eb4f4..54b8646 100644 --- a/src/Repository/Matchfunding/MatchSubmissionRepository.php +++ b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php @@ -2,22 +2,22 @@ namespace App\Repository\Matchfunding; -use App\Entity\Matchfunding\MatchSubmission; +use App\Entity\Matchfunding\MatchCallSubmission; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @extends ServiceEntityRepository + * @extends ServiceEntityRepository */ -class MatchSubmissionRepository extends ServiceEntityRepository +class MatchCallSubmissionRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, MatchSubmission::class); + parent::__construct($registry, MatchCallSubmission::class); } // /** - // * @return MatchSubmission[] Returns an array of MatchSubmission objects + // * @return MatchCallSubmission[] Returns an array of MatchCallSubmission objects // */ // public function findByExampleField($value): array // { @@ -31,7 +31,7 @@ class MatchSubmissionRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?MatchSubmission + // public function findOneBySomeField($value): ?MatchCallSubmission // { // return $this->createQueryBuilder('m') // ->andWhere('m.exampleField = :val') From e9ad56250ed0b325bb9180e98f40067442bc1388 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 3 Mar 2025 14:01:01 +0100 Subject: [PATCH 013/203] Fixes by php-cs-fixer --- src/ApiResource/Matchfunding/MatchCallApiResource.php | 6 +++--- .../Exception/MatchStrategyDuplicatedException.php | 3 +-- .../Exception/MatchStrategyNotFoundException.php | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index a5b688a..3006c88 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -30,14 +30,14 @@ class MatchCallApiResource public int $id; /** - * The Accounting which holds and spends the funds for this MatchCall. + * The Accounting which holds and spends the funds for this MatchCall. */ #[API\ApiProperty(writable: false)] public AccountingApiResource $accounting; /** * A list of Users who can modify this MatchCall. - * + * * @var UserApiResource[] */ #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALL_EDIT", object)')] @@ -45,7 +45,7 @@ class MatchCallApiResource /** * A list of the MatchCallSubmissions received by this MatchCall. - * + * * @var MatchCallSubmissionApiResource[] */ public array $matchCallSubmissions; diff --git a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php index c758224..df54fb6 100644 --- a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php +++ b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php @@ -10,9 +10,8 @@ class MatchStrategyDuplicatedException extends \Exception string $duplicatedName, string $duplicatedClass, string $strategyClass, - string $message = self::DUPLICATED_NAME + string $message = self::DUPLICATED_NAME, ) { - parent::__construct(\sprintf( $message, $duplicatedName, diff --git a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyNotFoundException.php b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyNotFoundException.php index 6cf10c3..36c3e6a 100644 --- a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyNotFoundException.php +++ b/src/Matchfunding/MatchStrategy/Exception/MatchStrategyNotFoundException.php @@ -9,7 +9,7 @@ class MatchStrategyNotFoundException extends \Exception public function __construct( string $name, string $message = self::MISSING_NAME, - ...$params + ...$params, ) { parent::__construct(\sprintf( $message, From 35a9f2b56e857715df6ae5acb97e9afa90178ad3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Apr 2025 14:08:16 +0200 Subject: [PATCH 014/203] Add Matchfunding group tag to redoc extension --- src/OpenApi/RedocExtensionsTrait.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index c285bee..8b6000e 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -39,6 +39,14 @@ trait RedocExtensionsTrait 'ProjectSupport', ], ], + [ + 'name' => 'Matchfunding', + 'tags' => [ + 'MatchCall', + 'MatchCallSubmission', + 'MatchStrategy' + ], + ], [ 'name' => 'Gateways', 'tags' => [ From 1f6bdb580ed960a31cb30990c5121793d2648c03 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Apr 2025 14:22:30 +0200 Subject: [PATCH 015/203] Update shape of matchcall submissions relationship --- .../Matchfunding/MatchCallApiResource.php | 19 ++--- .../MatchCallSubmissionApiResource.php | 2 +- src/Entity/Matchfunding/MatchCall.php | 74 +++++++++---------- .../Matchfunding/MatchCallSubmission.php | 14 ++-- 4 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 3006c88..17d5de8 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -35,18 +35,19 @@ class MatchCallApiResource #[API\ApiProperty(writable: false)] public AccountingApiResource $accounting; - /** - * A list of Users who can modify this MatchCall. - * - * @var UserApiResource[] - */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALL_EDIT", object)')] - public array $managers; - /** * A list of the MatchCallSubmissions received by this MatchCall. * * @var MatchCallSubmissionApiResource[] */ - public array $matchCallSubmissions; + #[API\ApiProperty(writable: false)] + public array $submissions; + + /** + * A list of Users who can modify this MatchCall. + * + * @var UserApiResource[] + */ + #[API\ApiProperty()] + public array $managers; } diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 3a6f8c8..46ad425 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -27,7 +27,7 @@ class MatchCallSubmissionApiResource /** * The MatchCall to which this MatchCallSubmission belongs to. */ - public MatchCallApiResource $matchCall; + public MatchCallApiResource $call; /** * The Project that applied for the MatchCall. diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 9545954..966b1d9 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -22,18 +22,18 @@ class MatchCall implements AccountingOwnerInterface #[ORM\JoinColumn(nullable: false)] private ?Accounting $accounting = null; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'matchCall', targetEntity: MatchCallSubmission::class)] + private Collection $submissions; + /** * @var Collection */ #[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; @@ -41,7 +41,7 @@ class MatchCall implements AccountingOwnerInterface { $this->accounting = Accounting::of($this); $this->managers = new ArrayCollection(); - $this->matchCallSubmissions = new ArrayCollection(); + $this->submissions = new ArrayCollection(); } public function getId(): ?int @@ -61,6 +61,36 @@ class MatchCall implements AccountingOwnerInterface return $this; } + /** + * @return Collection + */ + public function getSubmissions(): Collection + { + return $this->submissions; + } + + public function addSubmission(MatchCallSubmission $submission): static + { + if (!$this->submissions->contains($submission)) { + $this->submissions->add($submission); + $submission->setCall($this); + } + + return $this; + } + + public function removeSubmission(MatchCallSubmission $submission): static + { + if ($this->submissions->removeElement($submission)) { + // set the owning side to null (unless already changed) + if ($submission->getCall() === $this) { + $submission->setCall(null); + } + } + + return $this; + } + /** * @return Collection */ @@ -85,36 +115,6 @@ class MatchCall implements AccountingOwnerInterface 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; diff --git a/src/Entity/Matchfunding/MatchCallSubmission.php b/src/Entity/Matchfunding/MatchCallSubmission.php index 8823119..a86142b 100644 --- a/src/Entity/Matchfunding/MatchCallSubmission.php +++ b/src/Entity/Matchfunding/MatchCallSubmission.php @@ -14,11 +14,11 @@ class MatchCallSubmission #[ORM\Column] private ?int $id = null; - #[ORM\ManyToOne(inversedBy: 'matchCallSubmissions')] + #[ORM\ManyToOne(inversedBy: 'submissions')] #[ORM\JoinColumn(nullable: false)] - private ?MatchCall $matchCall = null; + private ?MatchCall $call = null; - #[ORM\ManyToOne(inversedBy: 'matchCallSubmissions')] + #[ORM\ManyToOne(inversedBy: 'submissions')] #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; @@ -30,14 +30,14 @@ class MatchCallSubmission return $this->id; } - public function getMatchCall(): ?MatchCall + public function getCall(): ?MatchCall { - return $this->matchCall; + return $this->call; } - public function setMatchCall(?MatchCall $matchCall): static + public function setCall(?MatchCall $call): static { - $this->matchCall = $matchCall; + $this->call = $call; return $this; } From 34930734a76337e78417b6d17641ab01fef792b3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Apr 2025 14:21:15 +0200 Subject: [PATCH 016/203] Update Territory object to be generic --- .../Project/ProjectApiResource.php | 3 +- .../Project/ProjectTerritoryApiResource.php | 33 ------------------- src/Benzina/ProjectsPump.php | 6 ++-- src/Dto/ProjectCreationDto.php | 4 +-- src/Dto/ProjectUpdationDto.php | 4 +-- src/Entity/Project/Project.php | 9 ++--- .../ProjectTerritory.php => Territory.php} | 27 ++++++++++----- src/Factory/Project/ProjectFactory.php | 6 ++-- src/OpenApi/RedocExtensionsTrait.php | 2 +- src/Service/Project/TerritoryService.php | 12 +++---- tests/Entity/ProjectApi/BaseTest.php | 4 +-- tests/Entity/ProjectApi/GetAllTest.php | 8 ++--- tests/Entity/ProjectCalendarTest.php | 4 +-- 13 files changed, 50 insertions(+), 72 deletions(-) delete mode 100644 src/ApiResource/Project/ProjectTerritoryApiResource.php rename src/Entity/{Project/ProjectTerritory.php => Territory.php} (53%) diff --git a/src/ApiResource/Project/ProjectApiResource.php b/src/ApiResource/Project/ProjectApiResource.php index 6b84d58..c1f5696 100644 --- a/src/ApiResource/Project/ProjectApiResource.php +++ b/src/ApiResource/Project/ProjectApiResource.php @@ -16,6 +16,7 @@ use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectVideo; +use App\Entity\Territory; use App\Mapping\Transformer\BudgetMapTransformer; use App\State\ApiResourceStateProvider; use App\State\Project\ProjectStateProcessor; @@ -102,7 +103,7 @@ class ProjectApiResource */ #[Assert\NotBlank()] #[Assert\Valid()] - public ProjectTerritoryApiResource $territory; + public Territory $territory; /** * Free-form rich text description for the Project. diff --git a/src/ApiResource/Project/ProjectTerritoryApiResource.php b/src/ApiResource/Project/ProjectTerritoryApiResource.php deleted file mode 100644 index db2b862..0000000 --- a/src/ApiResource/Project/ProjectTerritoryApiResource.php +++ /dev/null @@ -1,33 +0,0 @@ -cleanProjectLocation($record['project_location'], 2); if ($cleanAddress === '') { - return ProjectTerritory::unknown(); + return Territory::unknown(); } return $this->territoryService->search($cleanAddress); diff --git a/src/Dto/ProjectCreationDto.php b/src/Dto/ProjectCreationDto.php index 9f00edd..342b1e7 100644 --- a/src/Dto/ProjectCreationDto.php +++ b/src/Dto/ProjectCreationDto.php @@ -2,10 +2,10 @@ namespace App\Dto; -use App\ApiResource\Project\ProjectTerritoryApiResource; use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; +use App\Entity\Territory; use App\Mapping\Transformer\ProjectVideoMapTransformer; use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; @@ -35,7 +35,7 @@ class ProjectCreationDto */ #[Assert\NotBlank()] #[Assert\Valid()] - public ProjectTerritoryApiResource $territory; + public Territory $territory; /** * Free-form rich text description for the Project. diff --git a/src/Dto/ProjectUpdationDto.php b/src/Dto/ProjectUpdationDto.php index d02e4f6..b3f0535 100644 --- a/src/Dto/ProjectUpdationDto.php +++ b/src/Dto/ProjectUpdationDto.php @@ -3,11 +3,11 @@ namespace App\Dto; use ApiPlatform\Metadata as API; -use App\ApiResource\Project\ProjectTerritoryApiResource; use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; +use App\Entity\Territory; use App\Mapping\Transformer\ProjectVideoMapTransformer; use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; @@ -36,7 +36,7 @@ class ProjectUpdationDto * ISO 3166 data about the Project's territory of interest. */ #[Assert\Valid()] - public ProjectTerritoryApiResource $territory; + public Territory $territory; /** * Free-form rich text description for the Project. diff --git a/src/Entity/Project/Project.php b/src/Entity/Project/Project.php index becaf67..852eff8 100644 --- a/src/Entity/Project/Project.php +++ b/src/Entity/Project/Project.php @@ -7,6 +7,7 @@ use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Interface\UserOwnedInterface; use App\Entity\Matchfunding\MatchCallSubmission; +use App\Entity\Territory; use App\Entity\Trait\LocalizedEntityTrait; use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; @@ -63,8 +64,8 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized /** * Project's territory of interest. */ - #[ORM\Embedded(class: ProjectTerritory::class)] - private ?ProjectTerritory $territory; + #[ORM\Embedded(class: Territory::class)] + private ?Territory $territory; /** * The description body for the Project. @@ -211,12 +212,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this; } - public function getTerritory(): ?ProjectTerritory + public function getTerritory(): ?Territory { return $this->territory; } - public function setTerritory(ProjectTerritory $territory): static + public function setTerritory(Territory $territory): static { $this->territory = $territory; diff --git a/src/Entity/Project/ProjectTerritory.php b/src/Entity/Territory.php similarity index 53% rename from src/Entity/Project/ProjectTerritory.php rename to src/Entity/Territory.php index 274a744..d8bc3ae 100644 --- a/src/Entity/Project/ProjectTerritory.php +++ b/src/Entity/Territory.php @@ -1,30 +1,39 @@ subLvl2 = $subLvl2 ?? null; } - public static function unknown(): ProjectTerritory + public static function unknown(): Territory { - return new ProjectTerritory(self::COUNTRY_UNKNOWN_CODE); + return new Territory(self::COUNTRY_UNKNOWN_CODE); } } diff --git a/src/Factory/Project/ProjectFactory.php b/src/Factory/Project/ProjectFactory.php index e15adef..d347141 100644 --- a/src/Factory/Project/ProjectFactory.php +++ b/src/Factory/Project/ProjectFactory.php @@ -6,7 +6,7 @@ use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Entity\User\User; use App\Factory\User\UserFactory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; @@ -44,7 +44,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory 'category' => ProjectCategory::Design, 'deadline' => ProjectDeadline::Minimum, 'description' => '', - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'owner' => new User(), 'status' => ProjectStatus::InEditing, 'subtitle' => 'Subtitle', @@ -58,7 +58,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory 'category' => self::faker()->randomElement(ProjectCategory::cases()), 'deadline' => self::faker()->randomElement(ProjectDeadline::cases()), 'description' => self::faker()->text(), - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'owner' => UserFactory::createWithMode(), 'status' => self::faker()->randomElement(ProjectStatus::cases()), 'subtitle' => self::faker()->text(255), diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index 8b6000e..607bdff 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -44,7 +44,7 @@ trait RedocExtensionsTrait 'tags' => [ 'MatchCall', 'MatchCallSubmission', - 'MatchStrategy' + 'MatchStrategy', ], ], [ diff --git a/src/Service/Project/TerritoryService.php b/src/Service/Project/TerritoryService.php index c2f3b0f..f36fdcb 100644 --- a/src/Service/Project/TerritoryService.php +++ b/src/Service/Project/TerritoryService.php @@ -2,7 +2,7 @@ namespace App\Service\Project; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Service\Nominatim\NominatimService; class TerritoryService @@ -11,12 +11,12 @@ class TerritoryService private NominatimService $nominatimService, ) {} - public function search(string $query): ProjectTerritory + public function search(string $query): Territory { $search = $this->nominatimService->search($query); if (empty($search)) { - return ProjectTerritory::unknown(); + return Territory::unknown(); } $result = $search[0]; @@ -28,13 +28,13 @@ class TerritoryService $address = $result['address']; if (!\array_key_exists('country_code', $address)) { - return ProjectTerritory::unknown(); + return Territory::unknown(); } return $this->processResultAddress($address); } - private function processResultAddress(array $address): ProjectTerritory + private function processResultAddress(array $address): Territory { $country = \strtoupper($address['country_code']); @@ -56,6 +56,6 @@ class TerritoryService } } - return new ProjectTerritory($country, $subLvl1, $subLvl2); + return new Territory($country, $subLvl1, $subLvl2); } } diff --git a/tests/Entity/ProjectApi/BaseTest.php b/tests/Entity/ProjectApi/BaseTest.php index a87d059..a79d385 100644 --- a/tests/Entity/ProjectApi/BaseTest.php +++ b/tests/Entity/ProjectApi/BaseTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Symfony\Bundle\Test\Client; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Factory\Project\ProjectFactory; use App\Factory\User\UserFactory; use App\Tests\Traits\TestHelperTrait; @@ -79,7 +79,7 @@ abstract class BaseTest extends ApiTestCase protected function createTestProjectOptimized(int $count = 1, array $attributes = []): array { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $mergedAttributes = array_merge([ 'owner' => $owner, diff --git a/tests/Entity/ProjectApi/GetAllTest.php b/tests/Entity/ProjectApi/GetAllTest.php index c913adc..4a72d40 100644 --- a/tests/Entity/ProjectApi/GetAllTest.php +++ b/tests/Entity/ProjectApi/GetAllTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Factory\Project\ProjectFactory; use Symfony\Component\HttpFoundation\Response; @@ -52,7 +52,7 @@ class GetAllTest extends BaseTest int $responseCode = Response::HTTP_OK, ) { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $baseAttributes = [ 'owner' => $owner, 'territory' => $territory, @@ -81,7 +81,7 @@ class GetAllTest extends BaseTest string|ProjectCategory|ProjectStatus $otherValue, ) { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $baseAttributes = [ 'owner' => $owner, 'territory' => $territory, @@ -241,7 +241,7 @@ class GetAllTest extends BaseTest { ProjectFactory::createOne([ 'owner' => $this->createTestUser(), - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'title' => 'Lorem ipsum title', ]); diff --git a/tests/Entity/ProjectCalendarTest.php b/tests/Entity/ProjectCalendarTest.php index da45649..b680940 100644 --- a/tests/Entity/ProjectCalendarTest.php +++ b/tests/Entity/ProjectCalendarTest.php @@ -7,7 +7,7 @@ use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Entity\User\User; use Doctrine\ORM\EntityManagerInterface; use Zenstruck\Foundry\Test\ResetDatabase; @@ -47,7 +47,7 @@ class ProjectCalendarTest extends ApiTestCase $project->setDeadline($deadline); $project->setCategory(ProjectCategory::LibreSoftware); $project->setDescription('Test Project Description'); - $project->setTerritory(new ProjectTerritory('ES')); + $project->setTerritory(new Territory('ES')); $project->setOwner($this->owner); $project->setStatus(ProjectStatus::InReview); From 86108eaefaef573957c0c1407bdb6620ec4fd0be Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Apr 2025 17:15:27 +0200 Subject: [PATCH 017/203] Improve project location normalization of names variations --- src/Benzina/ProjectsPumpTrait.php | 68 ++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/src/Benzina/ProjectsPumpTrait.php b/src/Benzina/ProjectsPumpTrait.php index 1e51b2b..1069c2e 100644 --- a/src/Benzina/ProjectsPumpTrait.php +++ b/src/Benzina/ProjectsPumpTrait.php @@ -23,7 +23,7 @@ trait ProjectsPumpTrait // Remove secondary conjoined places from locations // e.g: "España y el mundo" -> "España" - foreach ([' / ', ' | ', ' - ', ' y ', ' and ', ' & '] as $conjoinment) { + foreach ([' / ', ' | ', ' - ', ' y ', ' i ', ' and ', ' & '] as $conjoinment) { if (\str_contains($location, $conjoinment)) { $location = \explode($conjoinment, $location)[0]; } @@ -41,18 +41,6 @@ trait ProjectsPumpTrait // Process comma-separated address pieces $location = \explode(',', $location); - $location = \array_map(function ($l) { - $l = trim($l); - - // Normalize typos and name variations - foreach (self::COMMON_VARIATIONS as $standard => $variations) { - if (in_array(\mb_strtoupper($l), $variations)) { - $l = $standard; - } - } - - return $l; - }, $location); // Clean non desired location pieces $location = \array_filter($location, function ($l) { @@ -68,11 +56,26 @@ trait ProjectsPumpTrait return true; }); + // Normalize location pieces + $location = \array_map(function ($l) { + // Trim spaces and numbers at start of piece + $l = \preg_replace('/^[\d ]+/', '', \trim($l)); + + // Normalize typos and name variations + foreach (self::COMMON_VARIATIONS as $standard => $variations) { + if (in_array(\mb_strtoupper($l), $variations)) { + $l = $standard; + } + } + + return $l; + }, $location); + $location = \join(', ', \array_slice($location, -1 * $detailLevel)); // Trim remaining numbers and punctuation marks - $location = \preg_replace('/^[\d\.\-;]+/', '', $location); - $location = \preg_replace('/[\d\.\-;]+$/', '', $location); + $location = \preg_replace('/^[\d\.\,\-;]+/', '', $location); + $location = \preg_replace('/[\d\.\,\-;]+$/', '', $location); return \mb_strtoupper(\trim($location)); } @@ -81,22 +84,47 @@ trait ProjectsPumpTrait * @var array The standard preferred name and a list of possible variations and misspellings */ private const COMMON_VARIATIONS = [ - 'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN'], - 'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'], - 'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'], + 'PERÚ' => ['PÉROU'], + 'ECUADOR' => ['EQUADOR'], + 'MÉXICO' => ['MESSICO', 'MX'], + 'CIUDAD DE MÉXICO' => ['CMDX'], + 'EUROPA' => ['EUROPEAN UNION'], 'FRANCIA' => ['FRANCE'], 'ITALIA' => ['ITALY'], 'ALEMANIA' => ['GERMANY'], + 'FINLANDIA' => ['FINLAND'], + 'SUECIA' => ['SWEDEN', 'SVERIGE'], + 'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN', 'ESTADO ESPAÑOL', 'ESPAINIA'], + 'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'], 'CÁDIZ' => ['CADIZ'], + 'CÓRDOBA' => ['CORDOBA'], + 'COMUNIDAD VALENCIANA' => ['COMUNITAT VALENCIANA', 'PAÍS VALENCIÀ'], 'VALENCIA' => ['VALÈNCIA', 'PROVINCIA DE VALENCIA'], 'ALICANTE' => ['ALACANT'], + 'CASTELLÓN DE LA PLANA' => ['CASTELLÓ DE LA PLANA'], + 'LAS PALMAS DE GRAN CANARIA' => ['LAS PALMAS'], 'CÁCERES' => ['CACERES'], + 'MÉRIDA' => ['MERIDA'], + 'LEÓN' => ['LEON'], + 'LEGANÉS' => ['LEGANES'], 'BALEARES' => ['BALEAREN', 'ISLAS BALEARES', 'ILLES BALEARS', 'BALEARIC ISLANDS'], 'GALICIA' => ['GALIZA'], - 'BILBAO' => ['BILBO'], + 'A CORUÑA' => ['LA CORUÑA'], + 'OURENSE' => ['ORENSE'], + 'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'], 'LLEIDA' => ['LÉRIDA'], - 'BARCELONA' => ['PROVINCIA DE BARCELONA'], + 'TARRASA' => ['TERRASA'], + 'BARCELONA' => ['PROVINCIA DE BARCELONA', 'BARCELONE', 'A BARCELONA'], + 'HOSPITALET DE LLOBREGAT' => ["L'HOSPITALET DE LLOBREGAT"], + 'SANT ADRIÀ DE BESÒS' => ['SANT ADRIÀ DEL BESÒS'], 'MURCIA' => ['REGIÓN DE MURCIA'], + 'PAÍS VASCO' => ['EUSKADI', 'EUSKAL HERRIA'], + 'BILBAO' => ['BILBO'], + 'SAN SEBASTIÁN' => ['DONOSTIA', 'DONOSTIA-SAN SEBASTIÁN'], + 'VIZCAYA' => ['BIZKAIA'], + 'IRÚN' => ['IRUN'], + 'NAVARRA' => ['NAVARRE', 'NAFARROA'], + 'PAMLONA' => ['IRUÑEA'], ]; private const PROJECT_KEYS = [ From debdd5194b1ae4a001b5b0cc9fd725be6e8d4f30 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Apr 2025 17:19:00 +0200 Subject: [PATCH 018/203] Add title, description and territory to match call --- .../Matchfunding/MatchCallApiResource.php | 21 ++++++++- src/Entity/Matchfunding/MatchCall.php | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 17d5de8..9f1e4cb 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -7,8 +7,10 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\Matchfunding\MatchCall; +use App\Entity\Territory; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use Symfony\Component\Validator\Constraints as Assert; /** * A MatchCall is an owned and managed event which accepts MatchCallSubmissions from Projects to receive *matchfunding* financement. @@ -48,6 +50,23 @@ class MatchCallApiResource * * @var UserApiResource[] */ - #[API\ApiProperty()] public array $managers; + + /** + * Main display title. + */ + #[Assert\NotBlank()] + public string $title; + + /** + * Long-form secondary display text. + */ + public string $description; + + /** + * Codes for the territory of interest in this MatchCall. + */ + #[Assert\NotBlank()] + #[Assert\Valid] + public Territory $territory; } diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 966b1d9..1277330 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -4,10 +4,12 @@ namespace App\Entity\Matchfunding; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Territory; use App\Entity\User\User; use App\Repository\Matchfunding\MatchCallRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MatchCallRepository::class)] @@ -37,6 +39,15 @@ class MatchCall implements AccountingOwnerInterface #[ORM\Column(length: 255)] private ?string $strategyName = null; + #[ORM\Column(length: 255)] + private ?string $title = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $description = null; + + #[ORM\Embedded(class: Territory::class)] + private ?Territory $territory = null; + public function __construct() { $this->accounting = Accounting::of($this); @@ -126,4 +137,40 @@ class MatchCall implements AccountingOwnerInterface return $this; } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getTerritory(): ?Territory + { + return $this->territory; + } + + public function setTerritory(Territory $territory): static + { + $this->territory = $territory; + + return $this; + } } From fbb80df4439aa3d648b236846ebb6aceaeb2ce0f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 21 Apr 2025 15:06:37 +0200 Subject: [PATCH 019/203] Rename MatchStrategy to Matchfunding\Formula --- config/services.yaml | 8 +-- .../Matchfunding/FormulaApiResource.php | 22 ++++++++ .../Matchfunding/MatchStrategyApiResource.php | 20 ------- .../MatchfundingTransactionsListener.php | 20 +++---- .../Exception/FormulaDuplicatedException.php} | 10 ++-- .../Exception/FormulaNotFoundException.php} | 6 +-- src/Matchfunding/Formula/FormulaInterface.php | 22 ++++++++ src/Matchfunding/Formula/FormulaLocator.php | 47 ++++++++++++++++ .../MatchStrategy/MatchStrategyInterface.php | 22 -------- .../MatchStrategy/MatchStrategyLocator.php | 53 ------------------- .../Matchfunding/FormulaStateProvider.php | 47 ++++++++++++++++ .../MatchStrategyStateProvider.php | 47 ---------------- 12 files changed, 160 insertions(+), 164 deletions(-) create mode 100644 src/ApiResource/Matchfunding/FormulaApiResource.php delete mode 100644 src/ApiResource/Matchfunding/MatchStrategyApiResource.php rename src/Matchfunding/{MatchStrategy/Exception/MatchStrategyDuplicatedException.php => Formula/Exception/FormulaDuplicatedException.php} (51%) rename src/Matchfunding/{MatchStrategy/Exception/MatchStrategyNotFoundException.php => Formula/Exception/FormulaNotFoundException.php} (54%) create mode 100644 src/Matchfunding/Formula/FormulaInterface.php create mode 100644 src/Matchfunding/Formula/FormulaLocator.php delete mode 100644 src/Matchfunding/MatchStrategy/MatchStrategyInterface.php delete mode 100644 src/Matchfunding/MatchStrategy/MatchStrategyLocator.php create mode 100644 src/State/Matchfunding/FormulaStateProvider.php delete mode 100644 src/State/Matchfunding/MatchStrategyStateProvider.php diff --git a/config/services.yaml b/config/services.yaml index 6163bad..6ec73c1 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -16,8 +16,8 @@ services: tags: ['app.gateway.gateway'] App\Library\Economy\Currency\ExchangeInterface: tags: ['app.lib.economy.currency.exchange'] - App\Matchfunding\MatchStrategy\MatchStrategyInterface: - tags: ['app.matchfunding.strategy.strategy'] + App\Matchfunding\Formula\FormulaInterface: + tags: ['app.matchfunding.formula.formula'] App\Service\Embed\Filter\EmbedFilterInterface: tags: ['app.embed.embed_filter'] App\Service\Embed\Provider\EmbedProviderFactoryInterface: @@ -61,9 +61,9 @@ services: - '%env(STRIPE_API_KEY)%' - '%env(STRIPE_WEBHOOK_SECRET)%' - App\Matchfunding\MatchStrategy\MatchStrategyLocator: + App\Matchfunding\Formula\FormulaLocator: arguments: - - !tagged 'app.matchfunding.strategy.strategy' + - !tagged 'app.matchfunding.formula.formula' App\Service\Auth\AuthService: arguments: diff --git a/src/ApiResource/Matchfunding/FormulaApiResource.php b/src/ApiResource/Matchfunding/FormulaApiResource.php new file mode 100644 index 0000000..1fd0ae6 --- /dev/null +++ b/src/ApiResource/Matchfunding/FormulaApiResource.php @@ -0,0 +1,22 @@ +getMatchCall(); - $strategy = $this->matchStrategyLocator->getForCall($call); - $match = $strategy->match($transaction); + // $call = $submission->getCall(); + // $strategy = $this->formulaLocator->get($call->getFormulaName()); + // $match = $strategy->match($transaction->getMoney()); - if ($match->getId() !== null) { - return; - } + // if ($match->getId() !== null) { + // return; + // } - $event->getObjectManager()->persist($match); - $event->getObjectManager()->flush(); + // $event->getObjectManager()->persist($match); + // $event->getObjectManager()->flush(); } } } diff --git a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php b/src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php similarity index 51% rename from src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php rename to src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php index df54fb6..6cb3e1d 100644 --- a/src/Matchfunding/MatchStrategy/Exception/MatchStrategyDuplicatedException.php +++ b/src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php @@ -1,22 +1,22 @@ */ + private array $strategiesByName = []; + + public function __construct( + iterable $strategies, + ) { + foreach ($strategies as $Formula) { + $FormulaName = $Formula::getName(); + + if (\array_key_exists($FormulaName, $this->strategiesByName)) { + throw new FormulaDuplicatedException( + $FormulaName, + $Formula::class, + $this->strategiesByName[$FormulaName]::class + ); + } + + $this->strategiesByName[$FormulaName] = $Formula; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->strategiesByName; + } + + public function get(string $FormulaName): ?FormulaInterface + { + if (!array_key_exists($FormulaName, $this->strategiesByName)) { + throw new FormulaNotFoundException($FormulaName); + } + + return $this->strategiesByName[$FormulaName]; + } +} diff --git a/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php b/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php deleted file mode 100644 index f373224..0000000 --- a/src/Matchfunding/MatchStrategy/MatchStrategyInterface.php +++ /dev/null @@ -1,22 +0,0 @@ - */ - 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/State/Matchfunding/FormulaStateProvider.php b/src/State/Matchfunding/FormulaStateProvider.php new file mode 100644 index 0000000..566e81d --- /dev/null +++ b/src/State/Matchfunding/FormulaStateProvider.php @@ -0,0 +1,47 @@ +formulaLocator->get($uriVariables['name']); + + return $this->getApiResource($formula); + } catch (FormulaNotFoundException $e) { + return null; + } + } + + $strategies = $this->formulaLocator->getAll(); + + $resources = []; + foreach ($strategies as $formula) { + $resources[] = $this->getApiResource($formula); + } + + return $resources; + } + + private function getApiResource(FormulaInterface $formula): FormulaApiResource + { + $resource = new FormulaApiResource(); + $resource->name = $formula::getName(); + + return $resource; + } +} diff --git a/src/State/Matchfunding/MatchStrategyStateProvider.php b/src/State/Matchfunding/MatchStrategyStateProvider.php deleted file mode 100644 index 338c2fb..0000000 --- a/src/State/Matchfunding/MatchStrategyStateProvider.php +++ /dev/null @@ -1,47 +0,0 @@ -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; - } -} From af5efe9516eb41d838b49bec822bab7fd269b832 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 12:54:44 +0200 Subject: [PATCH 020/203] Update references to formula --- src/Matchfunding/Formula/FormulaLocator.php | 22 ++++++++++----------- src/OpenApi/RedocExtensionsTrait.php | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Matchfunding/Formula/FormulaLocator.php b/src/Matchfunding/Formula/FormulaLocator.php index 1e53f67..353db05 100644 --- a/src/Matchfunding/Formula/FormulaLocator.php +++ b/src/Matchfunding/Formula/FormulaLocator.php @@ -13,18 +13,18 @@ class FormulaLocator public function __construct( iterable $strategies, ) { - foreach ($strategies as $Formula) { - $FormulaName = $Formula::getName(); + foreach ($strategies as $formula) { + $formulaName = $formula::getName(); - if (\array_key_exists($FormulaName, $this->strategiesByName)) { + if (\array_key_exists($formulaName, $this->strategiesByName)) { throw new FormulaDuplicatedException( - $FormulaName, - $Formula::class, - $this->strategiesByName[$FormulaName]::class + $formulaName, + $formula::class, + $this->strategiesByName[$formulaName]::class ); } - $this->strategiesByName[$FormulaName] = $Formula; + $this->strategiesByName[$formulaName] = $formula; } } @@ -36,12 +36,12 @@ class FormulaLocator return $this->strategiesByName; } - public function get(string $FormulaName): ?FormulaInterface + public function get(string $formulaName): ?FormulaInterface { - if (!array_key_exists($FormulaName, $this->strategiesByName)) { - throw new FormulaNotFoundException($FormulaName); + if (!array_key_exists($formulaName, $this->strategiesByName)) { + throw new FormulaNotFoundException($formulaName); } - return $this->strategiesByName[$FormulaName]; + return $this->strategiesByName[$formulaName]; } } diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index 607bdff..3f4d93b 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -44,7 +44,7 @@ trait RedocExtensionsTrait 'tags' => [ 'MatchCall', 'MatchCallSubmission', - 'MatchStrategy', + 'MatchFormula', ], ], [ From 391972c46b2988c2718a18019de7b3147e47863f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 13:57:45 +0200 Subject: [PATCH 021/203] Update FormulaInterface --- src/Matchfunding/Formula/FormulaInterface.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Matchfunding/Formula/FormulaInterface.php b/src/Matchfunding/Formula/FormulaInterface.php index 8706d89..9f10a3f 100644 --- a/src/Matchfunding/Formula/FormulaInterface.php +++ b/src/Matchfunding/Formula/FormulaInterface.php @@ -3,6 +3,7 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; +use Brick\Math\BigNumber; interface FormulaInterface { @@ -11,12 +12,19 @@ interface FormulaInterface */ public static function getName(): string; + /** + * The underlying math expressed as a common-notation formula. + */ + public static function getAsExpression(): string; + /** * Process an input Money and obtain a delta Money, which is the Money to be given by the Matcher. * - * @param Money $money The Money input that is to be matched + * @param BigNumber $factor A matching factor for the formula to use + * @param Money $money The input Money that is to be matched + * @param Money $limit An upper bound for the matching formula, the delta won't be higher than this * - * @return Money A Money delta + * @return Money The output Money delta */ - public function match(Money $money): Money; + public function match(BigNumber $factor, Money $money, Money $limit): Money; } From 430926462e04bb5edffb0a91dca97fb6a0494585 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 13:58:02 +0200 Subject: [PATCH 022/203] Expose formula expression in api resource --- src/ApiResource/Matchfunding/FormulaApiResource.php | 6 ++++++ src/State/Matchfunding/FormulaStateProvider.php | 1 + 2 files changed, 7 insertions(+) diff --git a/src/ApiResource/Matchfunding/FormulaApiResource.php b/src/ApiResource/Matchfunding/FormulaApiResource.php index 1fd0ae6..345136d 100644 --- a/src/ApiResource/Matchfunding/FormulaApiResource.php +++ b/src/ApiResource/Matchfunding/FormulaApiResource.php @@ -19,4 +19,10 @@ class FormulaApiResource { #[API\ApiProperty(identifier: true, writable: false)] public string $name; + + /** + * The underlying math expressed as a common-notation formula. + */ + #[API\ApiProperty(writable: false)] + public string $expression; } diff --git a/src/State/Matchfunding/FormulaStateProvider.php b/src/State/Matchfunding/FormulaStateProvider.php index 566e81d..1f21c9c 100644 --- a/src/State/Matchfunding/FormulaStateProvider.php +++ b/src/State/Matchfunding/FormulaStateProvider.php @@ -41,6 +41,7 @@ class FormulaStateProvider implements ProviderInterface { $resource = new FormulaApiResource(); $resource->name = $formula::getName(); + $resource->expression = $formula::getAsExpression(); return $resource; } From f036363406e1a8e86bbd9b37a1c116ac2aa7c975 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 13:58:36 +0200 Subject: [PATCH 023/203] Add multiplication and percentage formula implementations --- .../Formula/MultiplicationFormula.php | 31 ++++++++++++++++++ .../Formula/PercentageFormula.php | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/Matchfunding/Formula/MultiplicationFormula.php create mode 100644 src/Matchfunding/Formula/PercentageFormula.php diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php new file mode 100644 index 0000000..c4aee02 --- /dev/null +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -0,0 +1,31 @@ +multipliedBy($factor) + ); + + return MoneyService::toMoney($delta); + } +} diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php new file mode 100644 index 0000000..e92bd6c --- /dev/null +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -0,0 +1,32 @@ +multipliedBy($factor) + ->dividedBy(100) + ); + + return MoneyService::toMoney($delta); + } +} From e47d8aa244d2c70843d6cc9db579ca65ab08b94d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 14:00:32 +0200 Subject: [PATCH 024/203] Move match formula exceptions to generic matchfunding exceptions namespace --- .../{Formula => }/Exception/FormulaDuplicatedException.php | 2 +- .../{Formula => }/Exception/FormulaNotFoundException.php | 2 +- src/Matchfunding/Formula/FormulaLocator.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Matchfunding/{Formula => }/Exception/FormulaDuplicatedException.php (92%) rename src/Matchfunding/{Formula => }/Exception/FormulaNotFoundException.php (89%) diff --git a/src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php b/src/Matchfunding/Exception/FormulaDuplicatedException.php similarity index 92% rename from src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php rename to src/Matchfunding/Exception/FormulaDuplicatedException.php index 6cb3e1d..d5126e5 100644 --- a/src/Matchfunding/Formula/Exception/FormulaDuplicatedException.php +++ b/src/Matchfunding/Exception/FormulaDuplicatedException.php @@ -1,6 +1,6 @@ Date: Thu, 24 Apr 2025 17:12:06 +0200 Subject: [PATCH 025/203] Extract generic budget logic from map transformer to service --- .../Transformer/BudgetMapTransformer.php | 68 +++++-------------- src/Service/Project/BudgetService.php | 42 ++++++++++++ 2 files changed, 59 insertions(+), 51 deletions(-) create mode 100644 src/Service/Project/BudgetService.php diff --git a/src/Mapping/Transformer/BudgetMapTransformer.php b/src/Mapping/Transformer/BudgetMapTransformer.php index 9c5c019..c9af004 100644 --- a/src/Mapping/Transformer/BudgetMapTransformer.php +++ b/src/Mapping/Transformer/BudgetMapTransformer.php @@ -4,18 +4,17 @@ namespace App\Mapping\Transformer; use App\ApiResource\Project\Budget; use App\ApiResource\Project\BudgetSummary; -use App\Entity\Money; use App\Entity\Project\BudgetItem; use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; -use App\Library\Economy\MoneyService; +use App\Service\Project\BudgetService; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; class BudgetMapTransformer implements PropertyTransformerInterface { public function __construct( - private MoneyService $moneyService, + private BudgetService $budgetService, ) {} /** @@ -40,64 +39,31 @@ class BudgetMapTransformer implements PropertyTransformerInterface }); } - /** - * @param BudgetItem[] $items - * - * @return array{minimum: Money, optimum: Money} - */ - private function calcItemsTotal(array $items, string $currency): array - { - $totalMinimum = new Money(0, $currency); - $totalOptimum = new Money(0, $currency); - - foreach ($items as $item) { - $itemMoney = $item->getMoney(); - $itemDeadline = $item->getDeadline(); - - if ($itemDeadline == ProjectDeadline::Minimum) { - $totalMinimum = $this->moneyService->add($itemMoney, $totalMinimum); - } - - $totalOptimum = $this->moneyService->add($itemMoney, $totalOptimum); - } - - return [ - ProjectDeadline::Minimum->value => $totalMinimum, - ProjectDeadline::Optimum->value => $totalOptimum, - ]; - } - private function getItemsSummary(array $items, string $currency, ProjectDeadline $deadline) { - $deadlineValue = $deadline->value; + $deadline = $deadline->value; $summary = new BudgetSummary(); - $summary->money = $this->calcItemsTotal($items, $currency)[$deadlineValue]; - - $summary->task = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Task - ), + $summary->money = $this->budgetService->calcBudget( + $items, $currency - )[$deadlineValue]; + )[$deadline]; - $summary->material = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Material - ), + $summary->task = $this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Task), $currency - )[$deadlineValue]; + )[$deadline]; - $summary->infra = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Infrastructure - ), + $summary->material = $this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Material), $currency - )[$deadlineValue]; + )[$deadline]; + + $summary->infra = $this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Infrastructure), + $currency + )[$deadline]; return $summary; } diff --git a/src/Service/Project/BudgetService.php b/src/Service/Project/BudgetService.php new file mode 100644 index 0000000..4272f6e --- /dev/null +++ b/src/Service/Project/BudgetService.php @@ -0,0 +1,42 @@ +getMoney(); + $itemDeadline = $item->getDeadline(); + + if ($itemDeadline == ProjectDeadline::Minimum) { + $totalMinimum = $this->moneyService->add($itemMoney, $totalMinimum); + } + + $totalOptimum = $this->moneyService->add($itemMoney, $totalOptimum); + } + + return [ + ProjectDeadline::Minimum->value => $totalMinimum, + ProjectDeadline::Optimum->value => $totalOptimum, + ]; + } +} From 31e6440d8566ce8556688956fb3017d31dc1bb43 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 17:47:31 +0200 Subject: [PATCH 026/203] Add method to get filtered match call submissions --- src/Entity/Project/Project.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Entity/Project/Project.php b/src/Entity/Project/Project.php index 852eff8..a5ce595 100644 --- a/src/Entity/Project/Project.php +++ b/src/Entity/Project/Project.php @@ -7,6 +7,7 @@ use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Interface\UserOwnedInterface; use App\Entity\Matchfunding\MatchCallSubmission; +use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\Entity\Territory; use App\Entity\Trait\LocalizedEntityTrait; use App\Entity\Trait\MigratedEntity; @@ -310,6 +311,14 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this->matchCallSubmissions; } + /** + * @return Collection + */ + public function getMatchCallSubmissionsBy(MatchCallSubmissionStatus $status): Collection + { + return $this->matchCallSubmissions->filter(fn($s) => $s->getStatus() === $status); + } + public function addMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static { if (!$this->matchCallSubmissions->contains($MatchCallSubmission)) { From 1f39a112deb00e6f5106c51befba2ce3135441cc Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Apr 2025 17:47:54 +0200 Subject: [PATCH 027/203] Add enum to defined money to be matched --- src/Entity/Matchfunding/MatchAgainst.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Entity/Matchfunding/MatchAgainst.php diff --git a/src/Entity/Matchfunding/MatchAgainst.php b/src/Entity/Matchfunding/MatchAgainst.php new file mode 100644 index 0000000..eed0825 --- /dev/null +++ b/src/Entity/Matchfunding/MatchAgainst.php @@ -0,0 +1,24 @@ + Date: Fri, 25 Apr 2025 14:53:24 +0200 Subject: [PATCH 028/203] Add matchstrategy resource and entity --- .../Matchfunding/MatchStrategyApiResource.php | 40 ++++++++ src/Entity/Matchfunding/MatchStrategy.php | 99 +++++++++++++++++++ .../Matchfunding/MatchStrategyRepository.php | 43 ++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/ApiResource/Matchfunding/MatchStrategyApiResource.php create mode 100644 src/Entity/Matchfunding/MatchStrategy.php create mode 100644 src/Repository/Matchfunding/MatchStrategyRepository.php diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php new file mode 100644 index 0000000..115660d --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -0,0 +1,40 @@ +id; + } + + public function getCall(): ?MatchCall + { + return $this->call; + } + + public function setCall(MatchCall $call): static + { + $this->call = $call; + + return $this; + } + + public function getFormulaName(): ?string + { + return $this->formulaName; + } + + public function setFormulaName(string $formulaName): static + { + $this->formulaName = $formulaName; + + return $this; + } + + public function getLimit(): ?Money + { + return $this->limit; + } + + public function setLimit(Money $limit): static + { + $this->limit = $limit; + + return $this; + } + + public function getFactor(): ?BigNumber + { + return BigRational::of($this->factor); + } + + public function setFactor(BigNumber $factor): static + { + $this->factor = (string) $factor->toBigRational(); + + return $this; + } + + public function getAgainst(): ?MatchAgainst + { + return $this->against; + } + + public function setAgainst(MatchAgainst $against): static + { + $this->against = $against; + + return $this; + } +} diff --git a/src/Repository/Matchfunding/MatchStrategyRepository.php b/src/Repository/Matchfunding/MatchStrategyRepository.php new file mode 100644 index 0000000..a6975cf --- /dev/null +++ b/src/Repository/Matchfunding/MatchStrategyRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchStrategyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchStrategy::class); + } + +// /** +// * @return MatchStrategy[] Returns an array of MatchStrategy 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): ?MatchStrategy +// { +// return $this->createQueryBuilder('m') +// ->andWhere('m.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} From f7809d5aa93d729a2bad8d6b787f1da27c797c37 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 11:32:44 +0200 Subject: [PATCH 029/203] Relate MatchStrategy to MatchCall --- .../Matchfunding/MatchCallApiResource.php | 20 ++++++++++++++++--- src/Entity/Matchfunding/MatchCall.php | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 9f1e4cb..c27b061 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -13,12 +13,19 @@ use App\State\ApiResourceStateProvider; use Symfony\Component\Validator\Constraints as Assert; /** - * A MatchCall is an owned and managed event which accepts MatchCallSubmissions from Projects to receive *matchfunding* financement. - * This means any money inside a Transaction going to a Project in a MatchCall will be matched with funds from the MatchCall accounting. + * A MatchCall is a managed event which accepts MatchCallSubmissions from Projects to receive *matchfunding* financement. + * This means money going to a Project in a MatchCall can be matched with funds from the MatchCall accounting. * \ * \ * MatchCallSubmissions from Projects can be accepted or rejected by the managers. - * They can also choose from predefined strategies and tune them to perform the matching. + * \ + * \ + * How and when does a MatchCall match funds is determined by the MatchStrategy. + * Everytime there is a Charge item going to a Project accepted in a MatchCall the strategy is evaluated for matching. + * The strategy defines a series of rules that determine if the Charge is eligible for triggering the matching of funds. + * When a Charge is to be matched the strategy uses a MatchFormula, + * which is a predefined implementation for common mathematical operations used to match money. + * The formula is fine-tuned in the strategy by defining the variables in the formula. */ #[API\ApiResource( shortName: 'MatchCall', @@ -52,6 +59,13 @@ class MatchCallApiResource */ public array $managers; + /** + * The MatchStrategy defines the match behaviour for this MatchCall. + */ + #[Assert\NotBlank()] + #[Assert\Valid()] + public MatchStrategyApiResource $strategy; + /** * Main display title. */ diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 1277330..af5d987 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -24,6 +24,9 @@ class MatchCall implements AccountingOwnerInterface #[ORM\JoinColumn(nullable: false)] private ?Accounting $accounting = null; + #[ORM\OneToOne(mappedBy: 'matchCall', cascade: ['persist', 'remove'])] + private ?MatchStrategy $strategy = null; + /** * @var Collection */ @@ -72,6 +75,23 @@ class MatchCall implements AccountingOwnerInterface return $this; } + public function getStrategy(): ?MatchStrategy + { + return $this->strategy; + } + + public function setStrategy(MatchStrategy $strategy): static + { + // set the owning side of the relation if necessary + if ($strategy->getCall() !== $this) { + $strategy->setCall($this); + } + + $this->strategy = $strategy; + + return $this; + } + /** * @return Collection */ From 46755a0fd693be7169084b5dc577afb5ce0b88a5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 11:39:53 +0200 Subject: [PATCH 030/203] Update formulas expression to match method order of arguments --- src/Matchfunding/Formula/MultiplicationFormula.php | 2 +- src/Matchfunding/Formula/PercentageFormula.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php index c4aee02..ceb6cce 100644 --- a/src/Matchfunding/Formula/MultiplicationFormula.php +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -15,7 +15,7 @@ class MultiplicationFormula implements FormulaInterface public static function getAsExpression(): string { - return 'min(limit, factor * money)'; + return 'min(factor * money, limit)'; } public function match(BigNumber $factor, Money $money, Money $limit): Money diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php index e92bd6c..2ecbeb9 100644 --- a/src/Matchfunding/Formula/PercentageFormula.php +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -15,7 +15,7 @@ class PercentageFormula implements FormulaInterface public static function getAsExpression(): string { - return 'min(limit, factor / 100 * money)'; + return 'min(factor * money / 100, limit)'; } public function match(BigNumber $factor, Money $money, Money $limit): Money From 4a1f8b59a60a2c76277b226abcc99660aae6ccbf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 11:41:15 +0200 Subject: [PATCH 031/203] Add MatchfundingService --- .../Matchfunding/MatchfundingService.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/Service/Matchfunding/MatchfundingService.php diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php new file mode 100644 index 0000000..4b6af86 --- /dev/null +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -0,0 +1,53 @@ +getTarget()->getOwner(); + + if (!$target instanceof Project) { + return; + } + + foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { + $call = $submission->getCall(); + $strategy = $call->getStrategy(); + + $toBeMatched = match ($strategy->getAgainst()) { + MatchAgainst::Charge => $charge->getMoney(), + MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum], + MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum], + }; + + $matched = $this->formulaLocator + ->get($strategy->getFormulaName()) + ->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit()); + } + } + + private function getBudget(Project $project) + { + return $this->budgetService->calcBudget( + $project->getBudgetItems()->toArray(), + $project->getAccounting()->getCurrency() + ); + } +} From 834d79ce56c1d7775c2d2699c48988461048e6d2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 12:44:26 +0200 Subject: [PATCH 032/203] Style fixes by PHP CS Fixer --- .../Matchfunding/MatchCallApiResource.php | 2 +- .../Matchfunding/MatchStrategyApiResource.php | 3 +- .../Matchfunding/MatchStrategyRepository.php | 46 +++++++++---------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index c27b061..f5f516c 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -60,7 +60,7 @@ class MatchCallApiResource public array $managers; /** - * The MatchStrategy defines the match behaviour for this MatchCall. + * The MatchStrategy defines the match behaviour for this MatchCall. */ #[Assert\NotBlank()] #[Assert\Valid()] diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 115660d..95703b4 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -2,7 +2,6 @@ namespace App\ApiResource\Matchfunding; -use ApiPlatform\Metadata as API; use App\ApiResource\Money; use App\Entity\Matchfunding\MatchAgainst; use Symfony\Component\Validator\Constraints as Assert; @@ -33,7 +32,7 @@ class MatchStrategyApiResource * The money to be matched by the formula is * - `charge` the money in the Charge item * - `budget_min` the minimum in the Project's budget - * - `budget_opt` the optimum in the Project's budget + * - `budget_opt` the optimum in the Project's budget. */ #[Assert\NotBlank()] public MatchAgainst $match = MatchAgainst::Charge; diff --git a/src/Repository/Matchfunding/MatchStrategyRepository.php b/src/Repository/Matchfunding/MatchStrategyRepository.php index a6975cf..e86c34c 100644 --- a/src/Repository/Matchfunding/MatchStrategyRepository.php +++ b/src/Repository/Matchfunding/MatchStrategyRepository.php @@ -16,28 +16,28 @@ class MatchStrategyRepository extends ServiceEntityRepository parent::__construct($registry, MatchStrategy::class); } -// /** -// * @return MatchStrategy[] Returns an array of MatchStrategy 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() -// ; -// } + // /** + // * @return MatchStrategy[] Returns an array of MatchStrategy 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): ?MatchStrategy -// { -// return $this->createQueryBuilder('m') -// ->andWhere('m.exampleField = :val') -// ->setParameter('val', $value) -// ->getQuery() -// ->getOneOrNullResult() -// ; -// } + // public function findOneBySomeField($value): ?MatchStrategy + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } } From 8ad97e7956b036661e445f38191df0580a1ed730 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 12:44:49 +0200 Subject: [PATCH 033/203] Update outdated formula variables --- src/Matchfunding/Formula/FormulaLocator.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Matchfunding/Formula/FormulaLocator.php b/src/Matchfunding/Formula/FormulaLocator.php index 01b455f..a5006aa 100644 --- a/src/Matchfunding/Formula/FormulaLocator.php +++ b/src/Matchfunding/Formula/FormulaLocator.php @@ -8,23 +8,23 @@ use App\Matchfunding\Exception\FormulaNotFoundException; class FormulaLocator { /** @var array */ - private array $strategiesByName = []; + private array $formulasByName = []; public function __construct( - iterable $strategies, + iterable $formulas, ) { - foreach ($strategies as $formula) { + foreach ($formulas as $formula) { $formulaName = $formula::getName(); - if (\array_key_exists($formulaName, $this->strategiesByName)) { + if (\array_key_exists($formulaName, $this->formulasByName)) { throw new FormulaDuplicatedException( $formulaName, $formula::class, - $this->strategiesByName[$formulaName]::class + $this->formulasByName[$formulaName]::class ); } - $this->strategiesByName[$formulaName] = $formula; + $this->formulasByName[$formulaName] = $formula; } } @@ -33,15 +33,15 @@ class FormulaLocator */ public function getAll(): array { - return $this->strategiesByName; + return $this->formulasByName; } public function get(string $formulaName): ?FormulaInterface { - if (!array_key_exists($formulaName, $this->strategiesByName)) { + if (!array_key_exists($formulaName, $this->formulasByName)) { throw new FormulaNotFoundException($formulaName); } - return $this->strategiesByName[$formulaName]; + return $this->formulasByName[$formulaName]; } } From 21ea269d91f5a3eb055b07c10112a66776ed5050 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 13:00:39 +0200 Subject: [PATCH 034/203] Add Matchfunding Rules basics --- config/services.yaml | 10 +++- src/Matchfunding/Rule/RuleInterface.php | 19 +++++++ src/Matchfunding/Rule/RuleLocator.php | 50 +++++++++++++++++++ .../Matchfunding/MatchfundingService.php | 10 ++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/Matchfunding/Rule/RuleInterface.php create mode 100644 src/Matchfunding/Rule/RuleLocator.php diff --git a/config/services.yaml b/config/services.yaml index 6ec73c1..222a4a2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -17,7 +17,9 @@ services: App\Library\Economy\Currency\ExchangeInterface: tags: ['app.lib.economy.currency.exchange'] App\Matchfunding\Formula\FormulaInterface: - tags: ['app.matchfunding.formula.formula'] + tags: ['app.matchfunding.formula'] + App\Matchfunding\Rule\RuleInterface: + tags: ['app.matchfunding.rule'] App\Service\Embed\Filter\EmbedFilterInterface: tags: ['app.embed.embed_filter'] App\Service\Embed\Provider\EmbedProviderFactoryInterface: @@ -63,7 +65,11 @@ services: App\Matchfunding\Formula\FormulaLocator: arguments: - - !tagged 'app.matchfunding.formula.formula' + - !tagged 'app.matchfunding.formula' + + App\Matchfunding\Rule\RuleLocator: + arguments: + - !tagged 'app.matchfunding.rule' App\Service\Auth\AuthService: arguments: diff --git a/src/Matchfunding/Rule/RuleInterface.php b/src/Matchfunding/Rule/RuleInterface.php new file mode 100644 index 0000000..1d5202a --- /dev/null +++ b/src/Matchfunding/Rule/RuleInterface.php @@ -0,0 +1,19 @@ + */ + private array $rulesByClassName = []; + + public function __construct( + iterable $rules, + ) { + foreach ($rules as $rule) { + $this->rulesByClassName[$rule::class] = $rule; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->rulesByClassName; + } + + /** + * @return array + */ + public function getFrom(MatchStrategy $strategy): array + { + $rules = $strategy->getRuleClasses(); + + return \array_filter( + $this->rulesByClassName, + fn($r, $k) => \in_array($k, $rules) + ); + } + + public function getOne(string $className): ?RuleInterface + { + if (!array_key_exists($className, $this->rulesByClassName)) { + throw new ClassNotFoundException($className); + } + + return $this->rulesByClassName[$className]; + } +} diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index 4b6af86..e5aed63 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -8,6 +8,7 @@ use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Matchfunding\Formula\FormulaLocator; +use App\Matchfunding\Rule\RuleLocator; use App\Service\Project\BudgetService; class MatchfundingService @@ -15,6 +16,7 @@ class MatchfundingService public const SUBMISSION_ACCEPTED = MatchCallSubmissionStatus::Accepted; public function __construct( + private RuleLocator $ruleLocator, private FormulaLocator $formulaLocator, private BudgetService $budgetService, ) {} @@ -31,6 +33,14 @@ class MatchfundingService $call = $submission->getCall(); $strategy = $call->getStrategy(); + foreach ($strategy->getRuleClasses() as $ruleClass) { + $rule = $this->ruleLocator->get($ruleClass); + + if (!$rule->validate($charge, $target)) { + return; + } + } + $toBeMatched = match ($strategy->getAgainst()) { MatchAgainst::Charge => $charge->getMoney(), MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum], From 222b687e1d4eebda39e332ac505cc3f91bfb54b1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 13:12:06 +0200 Subject: [PATCH 035/203] Fix bug with namespace in formula state provider --- src/State/Matchfunding/FormulaStateProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/State/Matchfunding/FormulaStateProvider.php b/src/State/Matchfunding/FormulaStateProvider.php index 1f21c9c..d0d8077 100644 --- a/src/State/Matchfunding/FormulaStateProvider.php +++ b/src/State/Matchfunding/FormulaStateProvider.php @@ -5,7 +5,7 @@ namespace App\State\Matchfunding; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Matchfunding\FormulaApiResource; -use App\Matchfunding\Formula\Exception\FormulaNotFoundException; +use App\Matchfunding\Exception\FormulaNotFoundException; use App\Matchfunding\Formula\FormulaInterface; use App\Matchfunding\Formula\FormulaLocator; From 15b5040f257dd28604a3731c772dfce8ad7ebea7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:11:44 +0200 Subject: [PATCH 036/203] Make match strategy sub-resource of call --- .../Matchfunding/MatchCallApiResource.php | 13 ++++---- .../Matchfunding/MatchStrategyApiResource.php | 32 ++++++++++++++++++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index f5f516c..ebb11cf 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -44,6 +44,12 @@ class MatchCallApiResource #[API\ApiProperty(writable: false)] public AccountingApiResource $accounting; + /** + * The MatchStrategy defines the match behaviour for this MatchCall. + */ + #[API\ApiProperty(writable: false)] + public MatchStrategyApiResource $strategy; + /** * A list of the MatchCallSubmissions received by this MatchCall. * @@ -59,13 +65,6 @@ class MatchCallApiResource */ public array $managers; - /** - * The MatchStrategy defines the match behaviour for this MatchCall. - */ - #[Assert\NotBlank()] - #[Assert\Valid()] - public MatchStrategyApiResource $strategy; - /** * Main display title. */ diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 95703b4..9e40950 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -2,12 +2,43 @@ namespace App\ApiResource\Matchfunding; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata as API; use App\ApiResource\Money; use App\Entity\Matchfunding\MatchAgainst; +use App\Entity\Matchfunding\MatchStrategy; +use App\State\ApiResourceStateProcessor; +use App\State\ApiResourceStateProvider; use Symfony\Component\Validator\Constraints as Assert; +#[API\ApiResource( + shortName: 'MatchStrategy', + stateOptions: new Options(entityClass: MatchStrategy::class), + provider: ApiResourceStateProvider::class, + processor: ApiResourceStateProcessor::class, + uriTemplate: '/match_call/{id}/strategy', + uriVariables: [ + 'id' => new API\Link( + fromClass: MatchCallApiResource::class, + fromProperty: 'strategy', + description: 'MatchCall identifier' + ), + ] +)] +#[API\Get()] +#[API\Patch()] class MatchStrategyApiResource { + #[API\ApiProperty(identifier: true, writable: false)] + public MatchCallApiResource $call; + + /** + * The MatchRules used to validate the match making strategy. + * + * @var RuleApiResource[] + */ + public array $rules; + /** * The MatchFormula used to calculate matched funds. */ @@ -34,6 +65,5 @@ class MatchStrategyApiResource * - `budget_min` the minimum in the Project's budget * - `budget_opt` the optimum in the Project's budget. */ - #[Assert\NotBlank()] public MatchAgainst $match = MatchAgainst::Charge; } From 87698c962c301b8bebf6abe4799d42e759dc920f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:14:47 +0200 Subject: [PATCH 037/203] Add DEFAULT const to MatchAgainst enum --- src/ApiResource/Matchfunding/MatchStrategyApiResource.php | 2 +- src/Entity/Matchfunding/MatchAgainst.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 9e40950..d96df59 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -65,5 +65,5 @@ class MatchStrategyApiResource * - `budget_min` the minimum in the Project's budget * - `budget_opt` the optimum in the Project's budget. */ - public MatchAgainst $match = MatchAgainst::Charge; + public MatchAgainst $match = MatchAgainst::DEFAULT; } diff --git a/src/Entity/Matchfunding/MatchAgainst.php b/src/Entity/Matchfunding/MatchAgainst.php index eed0825..fc1959c 100644 --- a/src/Entity/Matchfunding/MatchAgainst.php +++ b/src/Entity/Matchfunding/MatchAgainst.php @@ -7,6 +7,8 @@ namespace App\Entity\Matchfunding; */ enum MatchAgainst: string { + public const DEFAULT = self::Charge; + /** * The money to be matched will be extracted from a Charge item. */ From ead9522a29da5bca9d7cd5aa6538b89ea2e3e1b7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:16:03 +0200 Subject: [PATCH 038/203] Update defaults of MatchStrategy entity --- src/Entity/Matchfunding/MatchStrategy.php | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index af05497..dece703 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -6,6 +6,7 @@ use App\Entity\Money; use App\Repository\Matchfunding\MatchStrategyRepository; use Brick\Math\BigNumber; use Brick\Math\BigRational; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MatchStrategyRepository::class)] @@ -20,17 +21,20 @@ class MatchStrategy #[ORM\JoinColumn(nullable: false)] private ?MatchCall $call = null; - #[ORM\Column(length: 255)] + #[ORM\Column(type: Types::ARRAY)] + private array $ruleClasses = []; + + #[ORM\Column(length: 255, nullable: true)] private ?string $formulaName = null; #[ORM\Embedded(class: Money::class)] private ?object $limit = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $factor = null; #[ORM\Column(enumType: MatchAgainst::class)] - private ?MatchAgainst $against = null; + private ?MatchAgainst $against = MatchAgainst::DEFAULT; public function getId(): ?int { @@ -49,6 +53,18 @@ class MatchStrategy return $this; } + public function getRuleClasses(): array + { + return $this->ruleClasses; + } + + public function setRuleClasses(array $ruleClasses): static + { + $this->ruleClasses = $ruleClasses; + + return $this; + } + public function getFormulaName(): ?string { return $this->formulaName; From 0c96b2aed127c5f9f50a47bdfd08e6250d5e17c4 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:16:38 +0200 Subject: [PATCH 039/203] Expose match rules as api resources --- .../Matchfunding/RuleApiResource.php | 28 ++++++++++ src/Matchfunding/Rule/RuleInterface.php | 5 ++ src/State/Matchfunding/RuleStateProvider.php | 56 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/ApiResource/Matchfunding/RuleApiResource.php create mode 100644 src/State/Matchfunding/RuleStateProvider.php diff --git a/src/ApiResource/Matchfunding/RuleApiResource.php b/src/ApiResource/Matchfunding/RuleApiResource.php new file mode 100644 index 0000000..009613b --- /dev/null +++ b/src/ApiResource/Matchfunding/RuleApiResource.php @@ -0,0 +1,28 @@ +ruleLocator->getOne(\sprintf( + 'App\\Matchfunding\\Rule\\%s', + $uriVariables['name'] + )); + + return $this->getApiResource($rule); + } catch (ClassNotFoundException $e) { + return null; + } + } + + $strategies = $this->ruleLocator->getAll(); + + $resources = []; + foreach ($strategies as $rule) { + $resources[] = $this->getApiResource($rule); + } + + return $resources; + } + + private function getApiResource(RuleInterface $rule): RuleApiResource + { + $resource = new RuleApiResource(); + $resource->name = self::getName($rule::class); + $resource->description = $rule::getDescription(); + + return $resource; + } + + private static function getName(string $class): string + { + return \array_slice(\explode('\\', $class), -1, 1)[0]; + } +} From d00d8859c393a94ab172c6bce3a5f74355127287 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:18:12 +0200 Subject: [PATCH 040/203] Add MatchStrategy and MatchRule to Redoc group --- src/OpenApi/RedocExtensionsTrait.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index 3f4d93b..a9fa08d 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -44,7 +44,9 @@ trait RedocExtensionsTrait 'tags' => [ 'MatchCall', 'MatchCallSubmission', + 'MatchStrategy', 'MatchFormula', + 'MatchRule', ], ], [ From c3a1125820486bf21e5a6f4d66cb760bed179ed8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 14:26:04 +0200 Subject: [PATCH 041/203] Update Match call and strategy entities relationship --- src/Entity/Matchfunding/MatchCall.php | 21 ++++----------------- src/Entity/Matchfunding/MatchStrategy.php | 8 ++++++++ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index af5d987..848a759 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -11,6 +11,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Strategy; #[ORM\Entity(repositoryClass: MatchCallRepository::class)] class MatchCall implements AccountingOwnerInterface @@ -24,13 +25,13 @@ class MatchCall implements AccountingOwnerInterface #[ORM\JoinColumn(nullable: false)] private ?Accounting $accounting = null; - #[ORM\OneToOne(mappedBy: 'matchCall', cascade: ['persist', 'remove'])] + #[ORM\OneToOne(mappedBy: 'call', cascade: ['persist', 'remove'])] private ?MatchStrategy $strategy = null; /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'matchCall', targetEntity: MatchCallSubmission::class)] + #[ORM\OneToMany(mappedBy: 'call', targetEntity: MatchCallSubmission::class)] private Collection $submissions; /** @@ -39,9 +40,6 @@ class MatchCall implements AccountingOwnerInterface #[ORM\ManyToMany(targetEntity: User::class)] private Collection $managers; - #[ORM\Column(length: 255)] - private ?string $strategyName = null; - #[ORM\Column(length: 255)] private ?string $title = null; @@ -54,6 +52,7 @@ class MatchCall implements AccountingOwnerInterface public function __construct() { $this->accounting = Accounting::of($this); + $this->strategy = MatchStrategy::of($this); $this->managers = new ArrayCollection(); $this->submissions = new ArrayCollection(); } @@ -146,18 +145,6 @@ class MatchCall implements AccountingOwnerInterface return $this; } - public function getStrategyName(): ?string - { - return $this->strategyName; - } - - public function setStrategyName(string $strategyName): static - { - $this->strategyName = $strategyName; - - return $this; - } - public function getTitle(): ?string { return $this->title; diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index dece703..3ec500b 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -36,6 +36,14 @@ class MatchStrategy #[ORM\Column(enumType: MatchAgainst::class)] private ?MatchAgainst $against = MatchAgainst::DEFAULT; + public static function of(MatchCall $call): MatchStrategy + { + $strategy = new MatchStrategy(); + $strategy->setCall($call); + + return $strategy; + } + public function getId(): ?int { return $this->id; From 28948375b64a11f638d4ab2fbfc1c78a31f8107b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 15:43:17 +0200 Subject: [PATCH 042/203] Add entity map providers for match call and match call submission --- src/Entity/Matchfunding/MatchCall.php | 4 +++- src/Entity/Matchfunding/MatchCallSubmission.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 848a759..b1ed62e 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -6,13 +6,15 @@ use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Territory; use App\Entity\User\User; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\Matchfunding\MatchCallRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Gedmo\Tree\Strategy; +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: MatchCallRepository::class)] class MatchCall implements AccountingOwnerInterface { diff --git a/src/Entity/Matchfunding/MatchCallSubmission.php b/src/Entity/Matchfunding/MatchCallSubmission.php index a86142b..3c36eb5 100644 --- a/src/Entity/Matchfunding/MatchCallSubmission.php +++ b/src/Entity/Matchfunding/MatchCallSubmission.php @@ -3,9 +3,12 @@ namespace App\Entity\Matchfunding; use App\Entity\Project\Project; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\Matchfunding\MatchCallSubmissionRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\ORM\Mapping as ORM; +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: MatchCallSubmissionRepository::class)] class MatchCallSubmission { From 8e6c0bae18d2e1a7b2c0e3d757e33be103e3e644 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 15:43:54 +0200 Subject: [PATCH 043/203] float data type enough for match formula factor --- src/Matchfunding/Formula/FormulaInterface.php | 5 ++--- src/Matchfunding/Formula/MultiplicationFormula.php | 3 +-- src/Matchfunding/Formula/PercentageFormula.php | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Matchfunding/Formula/FormulaInterface.php b/src/Matchfunding/Formula/FormulaInterface.php index 9f10a3f..0d6b066 100644 --- a/src/Matchfunding/Formula/FormulaInterface.php +++ b/src/Matchfunding/Formula/FormulaInterface.php @@ -3,7 +3,6 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; -use Brick\Math\BigNumber; interface FormulaInterface { @@ -20,11 +19,11 @@ interface FormulaInterface /** * Process an input Money and obtain a delta Money, which is the Money to be given by the Matcher. * - * @param BigNumber $factor A matching factor for the formula to use + * @param float $factor A matching factor for the formula to use * @param Money $money The input Money that is to be matched * @param Money $limit An upper bound for the matching formula, the delta won't be higher than this * * @return Money The output Money delta */ - public function match(BigNumber $factor, Money $money, Money $limit): Money; + public function match(float $factor, Money $money, Money $limit): Money; } diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php index ceb6cce..105f5fa 100644 --- a/src/Matchfunding/Formula/MultiplicationFormula.php +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -4,7 +4,6 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; use App\Library\Economy\MoneyService; -use Brick\Math\BigNumber; class MultiplicationFormula implements FormulaInterface { @@ -18,7 +17,7 @@ class MultiplicationFormula implements FormulaInterface return 'min(factor * money, limit)'; } - public function match(BigNumber $factor, Money $money, Money $limit): Money + public function match(float $factor, Money $money, Money $limit): Money { $delta = \Brick\Money\Money::min( MoneyService::toBrick($limit), diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php index 2ecbeb9..ecd1c0f 100644 --- a/src/Matchfunding/Formula/PercentageFormula.php +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -4,7 +4,6 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; use App\Library\Economy\MoneyService; -use Brick\Math\BigNumber; class PercentageFormula implements FormulaInterface { @@ -18,7 +17,7 @@ class PercentageFormula implements FormulaInterface return 'min(factor * money / 100, limit)'; } - public function match(BigNumber $factor, Money $money, Money $limit): Money + public function match(float $factor, Money $money, Money $limit): Money { $delta = \Brick\Money\Money::min( MoneyService::toBrick($limit), From a8e237ac6ee1d12701985901110c52f53abf95be Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 15:44:20 +0200 Subject: [PATCH 044/203] Update match strategy entity data --- src/Entity/Matchfunding/MatchStrategy.php | 26 +++++++------------ .../Provider/MatchStrategyMapProvider.php | 22 ++++++++++++++++ 2 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 src/Mapping/Provider/MatchStrategyMapProvider.php diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 3ec500b..3efb5c6 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -3,20 +3,17 @@ namespace App\Entity\Matchfunding; use App\Entity\Money; +use App\Mapping\Provider\MatchStrategyMapProvider; use App\Repository\Matchfunding\MatchStrategyRepository; -use Brick\Math\BigNumber; -use Brick\Math\BigRational; +use AutoMapper\Attribute\MapProvider; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +#[MapProvider(MatchStrategyMapProvider::class)] #[ORM\Entity(repositoryClass: MatchStrategyRepository::class)] class MatchStrategy { #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - private ?int $id = null; - #[ORM\OneToOne(inversedBy: 'strategy', cascade: ['persist', 'remove'])] #[ORM\JoinColumn(nullable: false)] private ?MatchCall $call = null; @@ -30,8 +27,8 @@ class MatchStrategy #[ORM\Embedded(class: Money::class)] private ?object $limit = null; - #[ORM\Column(length: 255, nullable: true)] - private ?string $factor = null; + #[ORM\Column(nullable: true)] + private ?float $factor = null; #[ORM\Column(enumType: MatchAgainst::class)] private ?MatchAgainst $against = MatchAgainst::DEFAULT; @@ -44,11 +41,6 @@ class MatchStrategy return $strategy; } - public function getId(): ?int - { - return $this->id; - } - public function getCall(): ?MatchCall { return $this->call; @@ -97,14 +89,14 @@ class MatchStrategy return $this; } - public function getFactor(): ?BigNumber + public function getFactor(): ?float { - return BigRational::of($this->factor); + return $this->factor; } - public function setFactor(BigNumber $factor): static + public function setFactor(float $factor): static { - $this->factor = (string) $factor->toBigRational(); + $this->factor = $factor; return $this; } diff --git a/src/Mapping/Provider/MatchStrategyMapProvider.php b/src/Mapping/Provider/MatchStrategyMapProvider.php new file mode 100644 index 0000000..5841ced --- /dev/null +++ b/src/Mapping/Provider/MatchStrategyMapProvider.php @@ -0,0 +1,22 @@ +matchStrategyRepository->find($source->call->id); + } +} From 1b76c981e3a65cea152598510361259174e12b63 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 15:53:54 +0200 Subject: [PATCH 045/203] Formula interface to resource in resource static method --- .../Matchfunding/FormulaApiResource.php | 10 ++++++++++ src/State/Matchfunding/FormulaStateProvider.php | 14 ++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ApiResource/Matchfunding/FormulaApiResource.php b/src/ApiResource/Matchfunding/FormulaApiResource.php index 345136d..f84873c 100644 --- a/src/ApiResource/Matchfunding/FormulaApiResource.php +++ b/src/ApiResource/Matchfunding/FormulaApiResource.php @@ -3,6 +3,7 @@ namespace App\ApiResource\Matchfunding; use ApiPlatform\Metadata as API; +use App\Matchfunding\Formula\FormulaInterface; use App\State\Matchfunding\FormulaStateProvider; /** @@ -25,4 +26,13 @@ class FormulaApiResource */ #[API\ApiProperty(writable: false)] public string $expression; + + public static function from(FormulaInterface $interface): FormulaApiResource + { + $resource = new FormulaApiResource(); + $resource->name = $interface::getName(); + $resource->expression = $interface::getAsExpression(); + + return $resource; + } } diff --git a/src/State/Matchfunding/FormulaStateProvider.php b/src/State/Matchfunding/FormulaStateProvider.php index d0d8077..a26556c 100644 --- a/src/State/Matchfunding/FormulaStateProvider.php +++ b/src/State/Matchfunding/FormulaStateProvider.php @@ -6,7 +6,6 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Matchfunding\FormulaApiResource; use App\Matchfunding\Exception\FormulaNotFoundException; -use App\Matchfunding\Formula\FormulaInterface; use App\Matchfunding\Formula\FormulaLocator; class FormulaStateProvider implements ProviderInterface @@ -21,7 +20,7 @@ class FormulaStateProvider implements ProviderInterface try { $formula = $this->formulaLocator->get($uriVariables['name']); - return $this->getApiResource($formula); + return FormulaApiResource::from($formula); } catch (FormulaNotFoundException $e) { return null; } @@ -31,18 +30,9 @@ class FormulaStateProvider implements ProviderInterface $resources = []; foreach ($strategies as $formula) { - $resources[] = $this->getApiResource($formula); + $resources[] = FormulaApiResource::from($formula); } return $resources; } - - private function getApiResource(FormulaInterface $formula): FormulaApiResource - { - $resource = new FormulaApiResource(); - $resource->name = $formula::getName(); - $resource->expression = $formula::getAsExpression(); - - return $resource; - } } From fc03b3bba09f9032affc2f66d38c0356636e944f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 15:54:57 +0200 Subject: [PATCH 046/203] MatchStrategy map formula entity formula name to formula resource --- .../Matchfunding/MatchStrategyApiResource.php | 5 +++++ .../MatchFormulaMapTransformer.php | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/Mapping/Transformer/MatchFormulaMapTransformer.php diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index d96df59..98635bc 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -7,8 +7,11 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Money; use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchStrategy; +use App\Mapping\Transformer\MatchFormulaMapTransformer; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use AutoMapper\Attribute\MapFrom; +use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; #[API\ApiResource( @@ -43,6 +46,8 @@ class MatchStrategyApiResource * The MatchFormula used to calculate matched funds. */ #[Assert\NotBlank()] + #[MapTo(MatchStrategy::class, property: 'formulaName', transformer: 'source.formula.name')] + #[MapFrom(MatchStrategy::class, property: 'formulaName', transformer: MatchFormulaMapTransformer::class)] public FormulaApiResource $formula; /** diff --git a/src/Mapping/Transformer/MatchFormulaMapTransformer.php b/src/Mapping/Transformer/MatchFormulaMapTransformer.php new file mode 100644 index 0000000..647123b --- /dev/null +++ b/src/Mapping/Transformer/MatchFormulaMapTransformer.php @@ -0,0 +1,21 @@ +formulaLocator->get($value); + + return FormulaApiResource::from($interface); + } +} From 5a5dd58b6f37bafc211b3d1498903cfdd49a32c5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:25:35 +0200 Subject: [PATCH 047/203] Rule interface to resource in resource static method --- .../Matchfunding/RuleApiResource.php | 10 ++++++++ src/State/Matchfunding/RuleStateProvider.php | 23 +++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/ApiResource/Matchfunding/RuleApiResource.php b/src/ApiResource/Matchfunding/RuleApiResource.php index 009613b..b5d72c2 100644 --- a/src/ApiResource/Matchfunding/RuleApiResource.php +++ b/src/ApiResource/Matchfunding/RuleApiResource.php @@ -3,6 +3,7 @@ namespace App\ApiResource\Matchfunding; use ApiPlatform\Metadata as API; +use App\Matchfunding\Rule\RuleInterface; use App\State\Matchfunding\RuleStateProvider; /** @@ -25,4 +26,13 @@ class RuleApiResource */ #[API\ApiProperty(writable: false)] public string $description; + + public static function from(RuleInterface $interface): RuleApiResource + { + $resource = new RuleApiResource(); + $resource->name = \array_slice(\explode('\\', $interface::class), -1, 1)[0]; + $resource->description = $interface::getDescription(); + + return $resource; + } } diff --git a/src/State/Matchfunding/RuleStateProvider.php b/src/State/Matchfunding/RuleStateProvider.php index 188c5e4..6376065 100644 --- a/src/State/Matchfunding/RuleStateProvider.php +++ b/src/State/Matchfunding/RuleStateProvider.php @@ -19,12 +19,9 @@ class RuleStateProvider implements ProviderInterface { if (\array_key_exists('name', $uriVariables)) { try { - $rule = $this->ruleLocator->getOne(\sprintf( - 'App\\Matchfunding\\Rule\\%s', - $uriVariables['name'] - )); + $rule = $this->ruleLocator->getOne($uriVariables['name']); - return $this->getApiResource($rule); + return RuleApiResource::from($rule); } catch (ClassNotFoundException $e) { return null; } @@ -34,23 +31,9 @@ class RuleStateProvider implements ProviderInterface $resources = []; foreach ($strategies as $rule) { - $resources[] = $this->getApiResource($rule); + $resources[] = RuleApiResource::from($rule); } return $resources; } - - private function getApiResource(RuleInterface $rule): RuleApiResource - { - $resource = new RuleApiResource(); - $resource->name = self::getName($rule::class); - $resource->description = $rule::getDescription(); - - return $resource; - } - - private static function getName(string $class): string - { - return \array_slice(\explode('\\', $class), -1, 1)[0]; - } } From ac947e1c29af5da9cebdda03894599ac31e99c05 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:25:57 +0200 Subject: [PATCH 048/203] Fixes in RuleLocator --- src/Matchfunding/Rule/RuleLocator.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Matchfunding/Rule/RuleLocator.php b/src/Matchfunding/Rule/RuleLocator.php index 9749102..2babd54 100644 --- a/src/Matchfunding/Rule/RuleLocator.php +++ b/src/Matchfunding/Rule/RuleLocator.php @@ -2,19 +2,22 @@ namespace App\Matchfunding\Rule; +use App\ApiResource\Matchfunding\RuleApiResource; use App\Entity\Matchfunding\MatchStrategy; use Symfony\Component\VarExporter\Exception\ClassNotFoundException; class RuleLocator { /** @var array */ - private array $rulesByClassName = []; + private array $rulesByName = []; public function __construct( iterable $rules, ) { foreach ($rules as $rule) { - $this->rulesByClassName[$rule::class] = $rule; + $name = RuleApiResource::from($rule)->name; + + $this->rulesByName[$name] = $rule; } } @@ -23,7 +26,7 @@ class RuleLocator */ public function getAll(): array { - return $this->rulesByClassName; + return $this->rulesByName; } /** @@ -34,17 +37,18 @@ class RuleLocator $rules = $strategy->getRuleClasses(); return \array_filter( - $this->rulesByClassName, - fn($r, $k) => \in_array($k, $rules) + $this->rulesByName, + fn($v, $k) => \in_array($k, $rules), + ARRAY_FILTER_USE_BOTH ); } public function getOne(string $className): ?RuleInterface { - if (!array_key_exists($className, $this->rulesByClassName)) { + if (!array_key_exists($className, $this->rulesByName)) { throw new ClassNotFoundException($className); } - return $this->rulesByClassName[$className]; + return $this->rulesByName[$className]; } } From 27019fbb7fb5f12863e7de887d007ce0d973788d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:27:36 +0200 Subject: [PATCH 049/203] MatchStrategy map rules entity names to rule resources --- .../Matchfunding/MatchStrategyApiResource.php | 13 ++++++++++++ .../Transformer/MatchRulesMapTransformer.php | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Mapping/Transformer/MatchRulesMapTransformer.php diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 98635bc..09b6200 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -8,6 +8,7 @@ use App\ApiResource\Money; use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchStrategy; use App\Mapping\Transformer\MatchFormulaMapTransformer; +use App\Mapping\Transformer\MatchRulesMapTransformer; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; use AutoMapper\Attribute\MapFrom; @@ -40,6 +41,8 @@ class MatchStrategyApiResource * * @var RuleApiResource[] */ + #[MapTo(MatchStrategy::class, property: 'ruleClasses', transformer: [self::class, 'rulesToNames'])] + #[MapFrom(MatchStrategy::class, property: 'ruleClasses', transformer: MatchRulesMapTransformer::class)] public array $rules; /** @@ -71,4 +74,14 @@ class MatchStrategyApiResource * - `budget_opt` the optimum in the Project's budget. */ public MatchAgainst $match = MatchAgainst::DEFAULT; + + /** + * @param array + * + * @return array + */ + public static function rulesToNames(array $values) + { + return \array_map(fn($r) => $r->name, $values); + } } diff --git a/src/Mapping/Transformer/MatchRulesMapTransformer.php b/src/Mapping/Transformer/MatchRulesMapTransformer.php new file mode 100644 index 0000000..804c374 --- /dev/null +++ b/src/Mapping/Transformer/MatchRulesMapTransformer.php @@ -0,0 +1,21 @@ +ruleLocator->getFrom($source); + + return \array_values(\array_map(fn($r) => RuleApiResource::from($r), $rules)); + } +} From 10a2c276c706f5dcbce949f23455e4e7908a5ea9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:29:19 +0200 Subject: [PATCH 050/203] Drop 'classes' from rule names referencing --- .../Matchfunding/MatchStrategyApiResource.php | 4 ++-- src/Entity/Matchfunding/MatchStrategy.php | 10 +++++----- src/Matchfunding/Rule/RuleLocator.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 09b6200..9798814 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -41,8 +41,8 @@ class MatchStrategyApiResource * * @var RuleApiResource[] */ - #[MapTo(MatchStrategy::class, property: 'ruleClasses', transformer: [self::class, 'rulesToNames'])] - #[MapFrom(MatchStrategy::class, property: 'ruleClasses', transformer: MatchRulesMapTransformer::class)] + #[MapTo(MatchStrategy::class, property: 'ruleNames', transformer: [self::class, 'rulesToNames'])] + #[MapFrom(MatchStrategy::class, property: 'ruleNames', transformer: MatchRulesMapTransformer::class)] public array $rules; /** diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 3efb5c6..0316f7d 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -19,7 +19,7 @@ class MatchStrategy private ?MatchCall $call = null; #[ORM\Column(type: Types::ARRAY)] - private array $ruleClasses = []; + private array $ruleNames = []; #[ORM\Column(length: 255, nullable: true)] private ?string $formulaName = null; @@ -53,14 +53,14 @@ class MatchStrategy return $this; } - public function getRuleClasses(): array + public function getRuleNames(): array { - return $this->ruleClasses; + return $this->ruleNames; } - public function setRuleClasses(array $ruleClasses): static + public function setRuleNames(array $ruleNames): static { - $this->ruleClasses = $ruleClasses; + $this->ruleNames = $ruleNames; return $this; } diff --git a/src/Matchfunding/Rule/RuleLocator.php b/src/Matchfunding/Rule/RuleLocator.php index 2babd54..2c7b84c 100644 --- a/src/Matchfunding/Rule/RuleLocator.php +++ b/src/Matchfunding/Rule/RuleLocator.php @@ -34,7 +34,7 @@ class RuleLocator */ public function getFrom(MatchStrategy $strategy): array { - $rules = $strategy->getRuleClasses(); + $rules = $strategy->getRuleNames(); return \array_filter( $this->rulesByName, From dcefe5ed79cd33743244f72755b86a24b7f5f9f9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:31:34 +0200 Subject: [PATCH 051/203] Update MatchService to take rules into account --- src/Service/Matchfunding/MatchfundingService.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index e5aed63..f0d729e 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -30,12 +30,9 @@ class MatchfundingService } foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { - $call = $submission->getCall(); - $strategy = $call->getStrategy(); - - foreach ($strategy->getRuleClasses() as $ruleClass) { - $rule = $this->ruleLocator->get($ruleClass); + $strategy = $submission->getCall()->getStrategy(); + foreach ($this->ruleLocator->getFrom($strategy) as $rule) { if (!$rule->validate($charge, $target)) { return; } From ca11deb7e4740e6a88551bdaac895e99c43e126b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:35:19 +0200 Subject: [PATCH 052/203] Style fixes by PHP CS Fixer --- src/ApiResource/Matchfunding/MatchStrategyApiResource.php | 4 ++-- src/Matchfunding/Formula/FormulaInterface.php | 4 ++-- src/Matchfunding/Rule/RuleInterface.php | 6 +++--- src/State/Matchfunding/RuleStateProvider.php | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 9798814..616a1f5 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -38,7 +38,7 @@ class MatchStrategyApiResource /** * The MatchRules used to validate the match making strategy. - * + * * @var RuleApiResource[] */ #[MapTo(MatchStrategy::class, property: 'ruleNames', transformer: [self::class, 'rulesToNames'])] @@ -77,7 +77,7 @@ class MatchStrategyApiResource /** * @param array - * + * * @return array */ public static function rulesToNames(array $values) diff --git a/src/Matchfunding/Formula/FormulaInterface.php b/src/Matchfunding/Formula/FormulaInterface.php index 0d6b066..461ed26 100644 --- a/src/Matchfunding/Formula/FormulaInterface.php +++ b/src/Matchfunding/Formula/FormulaInterface.php @@ -20,8 +20,8 @@ interface FormulaInterface * Process an input Money and obtain a delta Money, which is the Money to be given by the Matcher. * * @param float $factor A matching factor for the formula to use - * @param Money $money The input Money that is to be matched - * @param Money $limit An upper bound for the matching formula, the delta won't be higher than this + * @param Money $money The input Money that is to be matched + * @param Money $limit An upper bound for the matching formula, the delta won't be higher than this * * @return Money The output Money delta */ diff --git a/src/Matchfunding/Rule/RuleInterface.php b/src/Matchfunding/Rule/RuleInterface.php index 7ef85fc..3c15b9d 100644 --- a/src/Matchfunding/Rule/RuleInterface.php +++ b/src/Matchfunding/Rule/RuleInterface.php @@ -14,10 +14,10 @@ interface RuleInterface /** * Do necessary checks to determine if Charge and Project comply with this rule. - * - * @param Charge $charge The Charge that triggered the match making + * + * @param Charge $charge The Charge that triggered the match making * @param Project $project The Project that the Charge is going to - * + * * @return bool Whether or not this rule is met */ public function validate(Charge $charge, Project $project): bool; diff --git a/src/State/Matchfunding/RuleStateProvider.php b/src/State/Matchfunding/RuleStateProvider.php index 6376065..029daf1 100644 --- a/src/State/Matchfunding/RuleStateProvider.php +++ b/src/State/Matchfunding/RuleStateProvider.php @@ -5,7 +5,6 @@ namespace App\State\Matchfunding; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Matchfunding\RuleApiResource; -use App\Matchfunding\Rule\RuleInterface; use App\Matchfunding\Rule\RuleLocator; use Symfony\Component\VarExporter\Exception\ClassNotFoundException; From 032b48afb541149841b3b5c3b71eef7b7a21226d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Apr 2025 16:35:30 +0200 Subject: [PATCH 053/203] Add single user per project match making rule --- .../Rule/SingleUserPerProjectRule.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/Matchfunding/Rule/SingleUserPerProjectRule.php diff --git a/src/Matchfunding/Rule/SingleUserPerProjectRule.php b/src/Matchfunding/Rule/SingleUserPerProjectRule.php new file mode 100644 index 0000000..ffb9f9c --- /dev/null +++ b/src/Matchfunding/Rule/SingleUserPerProjectRule.php @@ -0,0 +1,33 @@ +chargeRepository->findBy([ + 'checkout.origin' => $charge->getCheckout()->getOrigin(), + 'target' => $project->getAccounting(), + ]); + + if (\count($charges) === 0) { + return true; + } + + return false; + } +} From e43840e475595c23c9cf484dc12a89ba2731fe4b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 12:05:46 +0200 Subject: [PATCH 054/203] Update match strategy api match against --- src/ApiResource/Matchfunding/MatchStrategyApiResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 616a1f5..eee38e2 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -73,7 +73,7 @@ class MatchStrategyApiResource * - `budget_min` the minimum in the Project's budget * - `budget_opt` the optimum in the Project's budget. */ - public MatchAgainst $match = MatchAgainst::DEFAULT; + public MatchAgainst $against = MatchAgainst::DEFAULT; /** * @param array From c3cb7cd49413f3d47d2890092a3ced03ab30c4d5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 12:16:05 +0200 Subject: [PATCH 055/203] Update match-making documentation --- .../Matchfunding/MatchStrategyApiResource.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index eee38e2..c5356d3 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -15,6 +15,16 @@ use AutoMapper\Attribute\MapFrom; use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; +/** + * A MatchStrategy defines how will a MatchCall perform the match-making process. + * \ + * The match-making flow is: + * 1. A Project accepted in a MatchCall receives a successful Charge. + * 2. The match-making loads the MatchCall's MatchStrategy. + * 3. The MatchStrategy rules are executed, if one rule fails the match-making is cancelled for the Charge. + * 3. The MatchStrategy formula function is passed the respective limit, factor and money of the MatchAgainst. + * 4. The result of the MatchStrategy formula function execution is put in a Transaction from the MatchCall to the Project. + */ #[API\ApiResource( shortName: 'MatchStrategy', stateOptions: new Options(entityClass: MatchStrategy::class), @@ -37,7 +47,7 @@ class MatchStrategyApiResource public MatchCallApiResource $call; /** - * The MatchRules used to validate the match making strategy. + * The MatchRules used to decide if the match making strategy should be executed or not. * * @var RuleApiResource[] */ From 4f3fe8eeb6bbb76252a7027c16a7ca8a072ddcfa Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 12:26:25 +0200 Subject: [PATCH 056/203] Update MatchCallSubmission default status --- .../MatchCallSubmissionApiResource.php | 6 ++--- .../MatchCallSubmissionStatus.php | 5 ++++ .../MatchCallSubmissionStateProcessor.php | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/State/Matchfunding/MatchCallSubmissionStateProcessor.php diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 46ad425..31cf1d1 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -7,8 +7,8 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Project\ProjectApiResource; use App\Entity\Matchfunding\MatchCallSubmission; use App\Entity\Matchfunding\MatchCallSubmissionStatus; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use App\State\Matchfunding\MatchCallSubmissionStateProcessor; /** * MatchCallSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement. @@ -17,7 +17,7 @@ use App\State\ApiResourceStateProvider; shortName: 'MatchCallSubmission', stateOptions: new Options(entityClass: MatchCallSubmission::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class + processor: MatchCallSubmissionStateProcessor::class )] class MatchCallSubmissionApiResource { @@ -39,5 +39,5 @@ class MatchCallSubmissionApiResource * Only MatchCallSubmissions with an status `accepted` will receive matchfunding. */ #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALLSUBMISSION_EDIT", object)')] - public MatchCallSubmissionStatus $status; + public MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::ToReview; } diff --git a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php index ce74d7c..5003fcc 100644 --- a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php +++ b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php @@ -4,6 +4,11 @@ namespace App\Entity\Matchfunding; enum MatchCallSubmissionStatus: string { + /** + * The MatchCallSubmission is pending review. + */ + case ToReview = 'to_review'; + /** * The MatchCallSubmission is under review for the MatchCall. */ diff --git a/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php new file mode 100644 index 0000000..dee0c74 --- /dev/null +++ b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php @@ -0,0 +1,26 @@ +autoMapper->map($data, MatchCallSubmission::class); + + dd($entity); + } +} From 0d0215ae4427a7d9eb1c05238a787d86c61453f0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 13:14:40 +0200 Subject: [PATCH 057/203] Create Dto for match call submission creation --- .../MatchCallSubmissionApiResource.php | 8 +++++++ .../MatchCallSubmissionCreationDto.php | 22 +++++++++++++++++++ .../MatchCallSubmissionStateProcessor.php | 6 ++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 31cf1d1..02e7127 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -5,6 +5,7 @@ namespace App\ApiResource\Matchfunding; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Project\ProjectApiResource; +use App\Dto\Matchfunding\MatchCallSubmissionCreationDto; use App\Entity\Matchfunding\MatchCallSubmission; use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\State\ApiResourceStateProvider; @@ -19,6 +20,11 @@ use App\State\Matchfunding\MatchCallSubmissionStateProcessor; provider: ApiResourceStateProvider::class, processor: MatchCallSubmissionStateProcessor::class )] +#[API\Post( + input: MatchCallSubmissionCreationDto::class, + securityPostDenormalize: 'is_granted("PROJECT_EDIT", object.project)', + securityPostDenormalizeMessage: 'You do not have permission to submit that Project to a MatchCall' +)] class MatchCallSubmissionApiResource { #[API\ApiProperty(identifier: true, writable: false)] @@ -27,11 +33,13 @@ class MatchCallSubmissionApiResource /** * The MatchCall to which this MatchCallSubmission belongs to. */ + #[API\ApiProperty(writable: false)] public MatchCallApiResource $call; /** * The Project that applied for the MatchCall. */ + #[API\ApiProperty(writable: false)] public ProjectApiResource $project; /** diff --git a/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php new file mode 100644 index 0000000..64827d8 --- /dev/null +++ b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php @@ -0,0 +1,22 @@ +autoMapper->map($data, MatchCallSubmission::class); - - dd($entity); } } From 5dfbfa582ea16d20c8c7ead0afd6099c5229f24f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 13:30:16 +0200 Subject: [PATCH 058/203] Revert submission state processing to default api resource processor --- .../MatchCallSubmissionApiResource.php | 13 +++++++--- .../Matchfunding/MatchCallSubmission.php | 2 +- .../MatchCallSubmissionStatus.php | 2 ++ .../MatchCallSubmissionStateProcessor.php | 26 ------------------- 4 files changed, 12 insertions(+), 31 deletions(-) delete mode 100644 src/State/Matchfunding/MatchCallSubmissionStateProcessor.php diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 02e7127..7738180 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -8,8 +8,8 @@ use App\ApiResource\Project\ProjectApiResource; use App\Dto\Matchfunding\MatchCallSubmissionCreationDto; use App\Entity\Matchfunding\MatchCallSubmission; use App\Entity\Matchfunding\MatchCallSubmissionStatus; +use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; -use App\State\Matchfunding\MatchCallSubmissionStateProcessor; /** * MatchCallSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement. @@ -18,13 +18,19 @@ use App\State\Matchfunding\MatchCallSubmissionStateProcessor; shortName: 'MatchCallSubmission', stateOptions: new Options(entityClass: MatchCallSubmission::class), provider: ApiResourceStateProvider::class, - processor: MatchCallSubmissionStateProcessor::class + processor: ApiResourceStateProcessor::class )] +#[API\GetCollection()] #[API\Post( input: MatchCallSubmissionCreationDto::class, securityPostDenormalize: 'is_granted("PROJECT_EDIT", object.project)', securityPostDenormalizeMessage: 'You do not have permission to submit that Project to a MatchCall' )] +#[API\Get()] +#[API\Patch( + security: 'is_granted("MATCHCALLSUBMISSION_EDIT", object)', + securityMessage: 'You do not have permission to edit this MatchCallSubmission' +)] class MatchCallSubmissionApiResource { #[API\ApiProperty(identifier: true, writable: false)] @@ -46,6 +52,5 @@ class MatchCallSubmissionApiResource * The status of the Project's application for the MatchCall.\ * Only MatchCallSubmissions with an status `accepted` will receive matchfunding. */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("MATCHCALLSUBMISSION_EDIT", object)')] - public MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::ToReview; + public MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::DEFAULT; } diff --git a/src/Entity/Matchfunding/MatchCallSubmission.php b/src/Entity/Matchfunding/MatchCallSubmission.php index 3c36eb5..141752f 100644 --- a/src/Entity/Matchfunding/MatchCallSubmission.php +++ b/src/Entity/Matchfunding/MatchCallSubmission.php @@ -26,7 +26,7 @@ class MatchCallSubmission private ?Project $project = null; #[ORM\Column(enumType: MatchCallSubmissionStatus::class)] - private ?MatchCallSubmissionStatus $status = null; + private ?MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::DEFAULT; public function getId(): ?int { diff --git a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php index 5003fcc..cd5c800 100644 --- a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php +++ b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php @@ -4,6 +4,8 @@ namespace App\Entity\Matchfunding; enum MatchCallSubmissionStatus: string { + public const DEFAULT = self::ToReview; + /** * The MatchCallSubmission is pending review. */ diff --git a/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php deleted file mode 100644 index ace9742..0000000 --- a/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php +++ /dev/null @@ -1,26 +0,0 @@ -autoMapper->map($data, MatchCallSubmission::class); - } -} From 5e21bfd4f5476e16fa7d87e3383a6826bb156dc6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 2 May 2025 13:49:54 +0200 Subject: [PATCH 059/203] Add match call voter --- .../MatchCallSubmissionApiResource.php | 2 +- src/Security/Voter/MatchCallVoter.php | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/Security/Voter/MatchCallVoter.php diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index 7738180..e453f2f 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -28,7 +28,7 @@ use App\State\ApiResourceStateProvider; )] #[API\Get()] #[API\Patch( - security: 'is_granted("MATCHCALLSUBMISSION_EDIT", object)', + security: 'is_granted("MATCHCALL_EDIT", object.call)', securityMessage: 'You do not have permission to edit this MatchCallSubmission' )] class MatchCallSubmissionApiResource diff --git a/src/Security/Voter/MatchCallVoter.php b/src/Security/Voter/MatchCallVoter.php new file mode 100644 index 0000000..aaa172c --- /dev/null +++ b/src/Security/Voter/MatchCallVoter.php @@ -0,0 +1,54 @@ +getUser(); + + switch ($attribute) { + case self::EDIT: + return $this->canEdit($subject, $user); + case self::VIEW: + return true; + } + + return false; + } + + private function canEdit(MatchCallApiResource $call, ?User $user): bool + { + if (!$user) { + return false; + } + + if ($user->hasRoles(['ROLE_ADMIN'])) { + return true; + } + + if (\in_array($user->getId(), \array_map(fn($m) => $m->id, $call->managers))) { + return true; + } + + return false; + } +} From 9e4a1f6a2615f223e687caf422ce19e8eb5484a2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 09:36:43 +0200 Subject: [PATCH 060/203] Add MatchCallSubmissionStateProcessor --- .../MatchCallSubmissionApiResource.php | 4 +-- .../MatchCallSubmissionStateProcessor.php | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/State/Matchfunding/MatchCallSubmissionStateProcessor.php diff --git a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php index e453f2f..10bcfbc 100644 --- a/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallSubmissionApiResource.php @@ -8,8 +8,8 @@ use App\ApiResource\Project\ProjectApiResource; use App\Dto\Matchfunding\MatchCallSubmissionCreationDto; use App\Entity\Matchfunding\MatchCallSubmission; use App\Entity\Matchfunding\MatchCallSubmissionStatus; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use App\State\Matchfunding\MatchCallSubmissionStateProcessor; /** * MatchCallSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement. @@ -18,7 +18,7 @@ use App\State\ApiResourceStateProvider; shortName: 'MatchCallSubmission', stateOptions: new Options(entityClass: MatchCallSubmission::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class + processor: MatchCallSubmissionStateProcessor::class )] #[API\GetCollection()] #[API\Post( diff --git a/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php new file mode 100644 index 0000000..b7a96a6 --- /dev/null +++ b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php @@ -0,0 +1,33 @@ +autoMapper->map($data, MatchCallSubmission::class); + $submission = $this->entityProcessor->process($submission, $operation, $uriVariables, $context); + + return $this->autoMapper->map($submission, MatchCallSubmissionApiResource::class); + } +} From 1472eb3da80223470bb9da733aee04032a442b27 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 09:37:01 +0200 Subject: [PATCH 061/203] Validate single submission to call per project --- .../MatchCallSubmissionCreationDto.php | 2 ++ .../SingleProjectSubmissionPerCall.php | 24 +++++++++++++ ...ingleProjectSubmissionPerCallValidator.php | 34 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/Validator/SingleProjectSubmissionPerCall.php create mode 100644 src/Validator/SingleProjectSubmissionPerCallValidator.php diff --git a/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php index 64827d8..9299538 100644 --- a/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php +++ b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php @@ -4,8 +4,10 @@ namespace App\Dto\Matchfunding; use App\ApiResource\Matchfunding\MatchCallApiResource; use App\ApiResource\Project\ProjectApiResource; +use App\Validator\SingleProjectSubmissionPerCall; use Symfony\Component\Validator\Constraints as Assert; +#[SingleProjectSubmissionPerCall()] class MatchCallSubmissionCreationDto { /** diff --git a/src/Validator/SingleProjectSubmissionPerCall.php b/src/Validator/SingleProjectSubmissionPerCall.php new file mode 100644 index 0000000..573374a --- /dev/null +++ b/src/Validator/SingleProjectSubmissionPerCall.php @@ -0,0 +1,24 @@ +matchCallSubmissionRepository->findOneBy([ + 'call' => $value->call->id, + 'project' => $value->project->id + ]); + + if ($submission === null) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} From 8bf84ca4f26ecf308ea90f073f0935c307e6d7e9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:07:06 +0200 Subject: [PATCH 062/203] Update Matchfunding event listening --- src/EventListener/MatchfundingListener.php | 26 +++++++++ .../MatchfundingTransactionsListener.php | 56 ------------------- 2 files changed, 26 insertions(+), 56 deletions(-) create mode 100644 src/EventListener/MatchfundingListener.php delete mode 100644 src/EventListener/MatchfundingTransactionsListener.php diff --git a/src/EventListener/MatchfundingListener.php b/src/EventListener/MatchfundingListener.php new file mode 100644 index 0000000..948b784 --- /dev/null +++ b/src/EventListener/MatchfundingListener.php @@ -0,0 +1,26 @@ +matchfunding->match($charge); + } +} diff --git a/src/EventListener/MatchfundingTransactionsListener.php b/src/EventListener/MatchfundingTransactionsListener.php deleted file mode 100644 index bba13fa..0000000 --- a/src/EventListener/MatchfundingTransactionsListener.php +++ /dev/null @@ -1,56 +0,0 @@ -getTarget()->getOwner(); - - if (!$target instanceof Project) { - return; - } - - $submissions = $target->getMatchCallSubmissions(); - - foreach ($submissions as $submission) { - if ($submission->getStatus() !== MatchCallSubmissionStatus::Accepted) { - continue; - } - - // $call = $submission->getCall(); - // $strategy = $this->formulaLocator->get($call->getFormulaName()); - // $match = $strategy->match($transaction->getMoney()); - - // if ($match->getId() !== null) { - // return; - // } - - // $event->getObjectManager()->persist($match); - // $event->getObjectManager()->flush(); - } - } -} From 70f735ef1e063ee41c78464afa47f5e1a49c759a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:08:19 +0200 Subject: [PATCH 063/203] Update matchfunding rule interface to validate scoped charges and submission --- src/Matchfunding/Rule/RuleInterface.php | 7 ++++--- src/Service/Matchfunding/MatchfundingService.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Matchfunding/Rule/RuleInterface.php b/src/Matchfunding/Rule/RuleInterface.php index 3c15b9d..326e8c2 100644 --- a/src/Matchfunding/Rule/RuleInterface.php +++ b/src/Matchfunding/Rule/RuleInterface.php @@ -3,6 +3,7 @@ namespace App\Matchfunding\Rule; use App\Entity\Gateway\Charge; +use App\Entity\Matchfunding\MatchCallSubmission; use App\Entity\Project\Project; interface RuleInterface @@ -15,10 +16,10 @@ interface RuleInterface /** * Do necessary checks to determine if Charge and Project comply with this rule. * - * @param Charge $charge The Charge that triggered the match making - * @param Project $project The Project that the Charge is going to + * @param Charge $charge The Charge that triggered the match making + * @param MatchCallSubmission $submission The MatchCallSubmission being validated * * @return bool Whether or not this rule is met */ - public function validate(Charge $charge, Project $project): bool; + public function validate(Charge $charge, MatchCallSubmission $submission): bool; } diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index f0d729e..3ecfd1a 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -33,7 +33,7 @@ class MatchfundingService $strategy = $submission->getCall()->getStrategy(); foreach ($this->ruleLocator->getFrom($strategy) as $rule) { - if (!$rule->validate($charge, $target)) { + if (!$rule->validate($charge, $submission)) { return; } } From be124080d72e73f334658641ba6e0bdef28ac0bc Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:08:43 +0200 Subject: [PATCH 064/203] Fix single user per project match rule validation --- .../Rule/SingleUserPerProjectRule.php | 12 +++--- src/Repository/Gateway/ChargeRepository.php | 42 ++++++++----------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/Matchfunding/Rule/SingleUserPerProjectRule.php b/src/Matchfunding/Rule/SingleUserPerProjectRule.php index ffb9f9c..d1e44fb 100644 --- a/src/Matchfunding/Rule/SingleUserPerProjectRule.php +++ b/src/Matchfunding/Rule/SingleUserPerProjectRule.php @@ -3,7 +3,7 @@ namespace App\Matchfunding\Rule; use App\Entity\Gateway\Charge; -use App\Entity\Project\Project; +use App\Entity\Matchfunding\MatchCallSubmission; use App\Repository\Gateway\ChargeRepository; class SingleUserPerProjectRule implements RuleInterface @@ -17,12 +17,12 @@ class SingleUserPerProjectRule implements RuleInterface return 'Validates that the matching is done only once per user per project.'; } - public function validate(Charge $charge, Project $project): bool + public function validate(Charge $charge, MatchCallSubmission $submission): bool { - $charges = $this->chargeRepository->findBy([ - 'checkout.origin' => $charge->getCheckout()->getOrigin(), - 'target' => $project->getAccounting(), - ]); + $charges = $this->chargeRepository->findByOriginAndTarget( + $charge->getCheckout()->getOrigin(), + $submission->getProject()->getAccounting() + ); if (\count($charges) === 0) { return true; diff --git a/src/Repository/Gateway/ChargeRepository.php b/src/Repository/Gateway/ChargeRepository.php index c8a196c..16e410c 100644 --- a/src/Repository/Gateway/ChargeRepository.php +++ b/src/Repository/Gateway/ChargeRepository.php @@ -2,8 +2,10 @@ namespace App\Repository\Gateway; +use App\Entity\Accounting\Accounting; use App\Entity\Gateway\Charge; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,28 +23,20 @@ class ChargeRepository extends ServiceEntityRepository parent::__construct($registry, Charge::class); } - // /** - // * @return Charge[] Returns an array of Charge objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('g.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?Charge - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + /** + * @return Charge[] Returns an array of Charge objects + */ + public function findByOriginAndTarget(Accounting $origin, Accounting $target): array + { + return $this->createQueryBuilder('g') + ->join('g.checkout', 'c', Join::WITH, 'c.id = g.checkout') + ->where('c.origin = :origin') + ->andWhere('g.target = :target') + ->setParameter('origin', $origin) + ->setParameter('target', $target) + ->orderBy('g.id', 'ASC') + ->getQuery() + ->getResult() + ; + } } From af76c1337e81858a37bf3b9e6009d8eed6954378 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:08:50 +0200 Subject: [PATCH 065/203] Code style fixes --- src/Validator/SingleProjectSubmissionPerCall.php | 2 +- src/Validator/SingleProjectSubmissionPerCallValidator.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Validator/SingleProjectSubmissionPerCall.php b/src/Validator/SingleProjectSubmissionPerCall.php index 573374a..bcc72ca 100644 --- a/src/Validator/SingleProjectSubmissionPerCall.php +++ b/src/Validator/SingleProjectSubmissionPerCall.php @@ -12,7 +12,7 @@ final class SingleProjectSubmissionPerCall extends Constraint public function __construct( public string $mode = 'strict', ?array $groups = null, - mixed $payload = null + mixed $payload = null, ) { parent::__construct([], $groups, $payload); } diff --git a/src/Validator/SingleProjectSubmissionPerCallValidator.php b/src/Validator/SingleProjectSubmissionPerCallValidator.php index 4dde0a4..413c21e 100644 --- a/src/Validator/SingleProjectSubmissionPerCallValidator.php +++ b/src/Validator/SingleProjectSubmissionPerCallValidator.php @@ -10,7 +10,7 @@ use Symfony\Component\Validator\ConstraintValidator; final class SingleProjectSubmissionPerCallValidator extends ConstraintValidator { public function __construct( - private MatchCallSubmissionRepository $matchCallSubmissionRepository + private MatchCallSubmissionRepository $matchCallSubmissionRepository, ) {} /** @@ -20,7 +20,7 @@ final class SingleProjectSubmissionPerCallValidator extends ConstraintValidator { $submission = $this->matchCallSubmissionRepository->findOneBy([ 'call' => $value->call->id, - 'project' => $value->project->id + 'project' => $value->project->id, ]); if ($submission === null) { From e094c01503e0317db6c1c7ed173249f8265083d0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:50:37 +0200 Subject: [PATCH 066/203] Update MatchCall owner of AccountingApiResource --- src/ApiResource/Accounting/AccountingApiResource.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ApiResource/Accounting/AccountingApiResource.php b/src/ApiResource/Accounting/AccountingApiResource.php index 29c26c5..336323b 100644 --- a/src/ApiResource/Accounting/AccountingApiResource.php +++ b/src/ApiResource/Accounting/AccountingApiResource.php @@ -4,9 +4,11 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; +use App\ApiResource\Matchfunding\MatchCallApiResource; use App\ApiResource\Project\ProjectApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\Accounting\Accounting; +use App\Entity\Matchfunding\MatchCall; use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Tipjar; @@ -67,6 +69,8 @@ class AccountingApiResource return $this->project; case Tipjar::class: return $this->tipjar; + case MatchCall::class: + return $this->matchCall; } return null; @@ -80,4 +84,7 @@ class AccountingApiResource #[API\ApiProperty(readable: false, writable: false)] public ?Tipjar $tipjar = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?MatchCallApiResource $matchCall; } From b9cb83f2c1b30db1f911e6509c12d45b124656ed Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 10:52:35 +0200 Subject: [PATCH 067/203] Update AccountingVoter default behaviour --- src/Security/Voter/AccountingVoter.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Security/Voter/AccountingVoter.php b/src/Security/Voter/AccountingVoter.php index a681126..a41cce7 100644 --- a/src/Security/Voter/AccountingVoter.php +++ b/src/Security/Voter/AccountingVoter.php @@ -3,7 +3,6 @@ namespace App\Security\Voter; use App\ApiResource\Accounting\AccountingApiResource; -use App\ApiResource\Project\ProjectApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\User\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -31,21 +30,21 @@ class AccountingVoter extends Voter $owner = $subject->getOwner(); switch ($owner::class) { - case ProjectApiResource::class: - return $this->voteOnProject($attribute, $owner, $user); case UserApiResource::class: return $this->voteOnUser($attribute, $owner, $user); + default: + return $this->voteOn($attribute, $subject, $user); } return false; } - private function voteOnProject(string $attribute, ProjectApiResource $project, ?User $user): bool + private function voteOn(string $attribute, mixed $subject, ?User $user): bool { switch ($attribute) { case self::EDIT: return $user->hasRoles(['ROLE_ADMIN']) - || $this->isOwnerOf($project, $user); + || $this->isOwnerOf($subject, $user); case self::VIEW: return true; } From bc3e0a2786392935821b464461dd1d8896ebb39b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 11:22:38 +0200 Subject: [PATCH 068/203] Move Charge default status from constructor to property --- src/Entity/Gateway/Charge.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index 802396c..f89a3a2 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -85,12 +85,11 @@ class Charge */ #[Gedmo\Versioned] #[ORM\Column()] - private ?ChargeStatus $status = null; + private ?ChargeStatus $status = ChargeStatus::Pending; public function __construct() { $this->transactions = new ArrayCollection(); - $this->status = ChargeStatus::Pending; } public function getId(): ?int From 6b17cd9816a96d03a708f098e3975940b58c8ae8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 12:41:16 +0200 Subject: [PATCH 069/203] Fix matching process --- src/EventListener/MatchfundingListener.php | 53 +++++++++++++++++-- .../Matchfunding/MatchfundingService.php | 28 ++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/EventListener/MatchfundingListener.php b/src/EventListener/MatchfundingListener.php index 948b784..b074fb9 100644 --- a/src/EventListener/MatchfundingListener.php +++ b/src/EventListener/MatchfundingListener.php @@ -3,24 +3,67 @@ namespace App\EventListener; use App\Entity\Gateway\Charge; +use App\Gateway\ChargeStatus; use App\Service\Matchfunding\MatchfundingService; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; -use Doctrine\ORM\Event\PostPersistEventArgs; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Events; #[AsEntityListener( - event: Events::postPersist, - method: 'processMatchfunding', + event: Events::preUpdate, + method: 'preUpdate', + entity: Charge::class +)] +#[AsEntityListener( + event: Events::postUpdate, + method: 'postUpdate', entity: Charge::class )] final class MatchfundingListener { + /** + * @var Charge|null + */ + private ?Charge $charge = null; + public function __construct( private MatchfundingService $matchfunding, + private EntityManagerInterface $entityManager, ) {} - public function processMatchfunding(Charge $charge, PostPersistEventArgs $event) + public function preUpdate(Charge $charge, PreUpdateEventArgs $event) { - $this->matchfunding->match($charge); + $this->charge = null; + + if (count($event->getEntityChangeSet()) === 0) { + return; + } + + if ($charge->getStatus() !== ChargeStatus::Charged) { + return; + } + + $transactions = $this->matchfunding->match($charge); + + if (\count($transactions) > 0) { + foreach ($transactions as $transaction) { + $charge->addTransaction($transaction); + } + + $this->charge = $charge; + } + } + + public function postUpdate() + { + if ($this->charge === null) { + return; + } + + $this->entityManager->persist($this->charge); + $this->entityManager->flush(); + + $this->charge = null; } } diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index 3ecfd1a..72b9144 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -2,9 +2,11 @@ namespace App\Service\Matchfunding; +use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchCallSubmissionStatus; +use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Matchfunding\Formula\FormulaLocator; @@ -21,33 +23,49 @@ class MatchfundingService private BudgetService $budgetService, ) {} - public function match(Charge $charge): void + /** + * Perform the match-making logic for a Charge. + * + * @return Transaction[] The Transactions from the MatchCalls that should be made for a matcheable Charge + */ + public function match(Charge $charge): array { $target = $charge->getTarget()->getOwner(); if (!$target instanceof Project) { - return; + return []; } + $transactions = []; + foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { $strategy = $submission->getCall()->getStrategy(); foreach ($this->ruleLocator->getFrom($strategy) as $rule) { if (!$rule->validate($charge, $submission)) { - return; + continue; } } $toBeMatched = match ($strategy->getAgainst()) { MatchAgainst::Charge => $charge->getMoney(), - MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum], - MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum], + MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum->value], + MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum->value], }; $matched = $this->formulaLocator ->get($strategy->getFormulaName()) ->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit()); + + $transaction = new Transaction(); + $transaction->setMoney($matched); + $transaction->setOrigin($submission->getCall()->getAccounting()); + $transaction->setTarget($submission->getProject()->getAccounting()); + + $transactions[] = $transaction; } + + return $transactions; } private function getBudget(Project $project) From bdb3d7ad37142822caa0b4c3a95004821b67b0cd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 5 May 2025 12:44:55 +0200 Subject: [PATCH 070/203] Code style fixes by CS-Fixer --- src/EventListener/MatchfundingListener.php | 3 --- src/Service/Matchfunding/MatchfundingService.php | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/EventListener/MatchfundingListener.php b/src/EventListener/MatchfundingListener.php index b074fb9..711b773 100644 --- a/src/EventListener/MatchfundingListener.php +++ b/src/EventListener/MatchfundingListener.php @@ -22,9 +22,6 @@ use Doctrine\ORM\Events; )] final class MatchfundingListener { - /** - * @var Charge|null - */ private ?Charge $charge = null; public function __construct( diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index 72b9144..97159b8 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -6,7 +6,6 @@ use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchCallSubmissionStatus; -use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Matchfunding\Formula\FormulaLocator; @@ -25,7 +24,7 @@ class MatchfundingService /** * Perform the match-making logic for a Charge. - * + * * @return Transaction[] The Transactions from the MatchCalls that should be made for a matcheable Charge */ public function match(Charge $charge): array From 484abf7313bffca299623308121349002ef25e92 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 15 May 2025 17:41:36 +0200 Subject: [PATCH 071/203] Update locale pumping based on parent project locale --- src/Benzina/ProjectsBudgetPump.php | 4 ++-- src/Benzina/ProjectsRewardsPump.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Benzina/ProjectsBudgetPump.php b/src/Benzina/ProjectsBudgetPump.php index 52908b0..e2805f8 100644 --- a/src/Benzina/ProjectsBudgetPump.php +++ b/src/Benzina/ProjectsBudgetPump.php @@ -49,7 +49,7 @@ class ProjectsBudgetPump implements PumpInterface return; } - $project = $this->getBudgetProject($record); + $project = $this->getProject($record); if ($project === null) { return; } @@ -66,7 +66,7 @@ class ProjectsBudgetPump implements PumpInterface $this->persist($budgetItem, $context); } - private function getBudgetProject(array $record): ?Project + private function getProject(array $record): ?Project { return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); } diff --git a/src/Benzina/ProjectsRewardsPump.php b/src/Benzina/ProjectsRewardsPump.php index 8f17ae8..cf29234 100644 --- a/src/Benzina/ProjectsRewardsPump.php +++ b/src/Benzina/ProjectsRewardsPump.php @@ -59,12 +59,13 @@ class ProjectsRewardsPump implements PumpInterface return; } - $project = $this->getBudgetProject($record); + $project = $this->getProject($record); if ($project === null) { return; } $reward = new Reward(); + $reward->setTranslatableLocale($project->getLocales()[0]); $reward->setProject($project); $reward->setTitle($record['reward']); $reward->setDescription($record['description'] ?? $record['reward']); @@ -76,7 +77,7 @@ class ProjectsRewardsPump implements PumpInterface $this->persist($reward, $context); } - private function getBudgetProject(array $record): ?Project + private function getProject(array $record): ?Project { return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); } From 09d5447005977400a00aa4fa3b613427b7a0ea5a Mon Sep 17 00:00:00 2001 From: cmorenov1402 Date: Tue, 20 May 2025 09:22:14 +0200 Subject: [PATCH 072/203] Add partial search filter to name --- src/ApiResource/TipjarApiResource.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ApiResource/TipjarApiResource.php b/src/ApiResource/TipjarApiResource.php index 5b58421..90a877d 100644 --- a/src/ApiResource/TipjarApiResource.php +++ b/src/ApiResource/TipjarApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource; +use ApiPlatform\Doctrine\Orm\Filter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; @@ -38,5 +39,6 @@ class TipjarApiResource * Human readable, non white space, unique string. */ #[Assert\NotBlank()] + #[API\ApiFilter(Filter\SearchFilter::class, strategy: 'partial')] public string $name; } From afab73aee4f35060537be969b5b3602aad35d9b1 Mon Sep 17 00:00:00 2001 From: cmorenov1402 Date: Thu, 22 May 2025 10:39:44 +0200 Subject: [PATCH 073/203] Add date filter to charge date updated and created --- src/ApiResource/Gateway/ChargeApiResource.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index 35b1533..1b52975 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -87,6 +87,9 @@ class ChargeApiResource #[API\ApiFilter(Filter\SearchFilter::class, strategy: 'exact')] public ChargeStatus $status = ChargeStatus::InPending; + #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateCreated; + + #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateUpdated; } From e48de8f5424df1cad001f17e27ea7449482daf3c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 22 May 2025 12:07:21 +0200 Subject: [PATCH 074/203] Missing money.currency filtering --- src/ApiResource/Gateway/ChargeApiResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index 35b1533..8c535f1 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -76,8 +76,8 @@ class ChargeApiResource * It is money before fees and taxes, not accountable. */ #[Assert\NotBlank()] - #[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.amount' => 'exact'])] #[API\ApiFilter(Filter\RangeFilter::class, properties: ['money.amount'])] + #[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.currency' => 'exact'])] public Money $money; /** From a5db0caf5b5d224b55ef6fba7f92d7f6b7d9d63b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 22 May 2025 13:23:29 +0200 Subject: [PATCH 075/203] Make Checkout Trackings into their own entity --- src/Entity/Gateway/Tracking.php | 78 +++++++++++++++++++ src/Gateway/Tracking.php | 17 ---- src/Mapping/Provider/TrackingMapProvider.php | 25 ++++++ src/Repository/Gateway/TrackingRepository.php | 43 ++++++++++ 4 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/Entity/Gateway/Tracking.php create mode 100644 src/Mapping/Provider/TrackingMapProvider.php create mode 100644 src/Repository/Gateway/TrackingRepository.php diff --git a/src/Entity/Gateway/Tracking.php b/src/Entity/Gateway/Tracking.php new file mode 100644 index 0000000..51229d6 --- /dev/null +++ b/src/Entity/Gateway/Tracking.php @@ -0,0 +1,78 @@ +title = $title ?? $title; + $this->value = $value ?? $value; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCheckout(): ?Checkout + { + return $this->checkout; + } + + public function setCheckout(?Checkout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(string $value): static + { + $this->value = $value; + + return $this; + } +} diff --git a/src/Gateway/Tracking.php b/src/Gateway/Tracking.php index 0f48007..b611781 100644 --- a/src/Gateway/Tracking.php +++ b/src/Gateway/Tracking.php @@ -2,32 +2,15 @@ namespace App\Gateway; -use Symfony\Component\Validator\Constraints as Assert; - class Tracking { /** * A descriptive title for the tracking number. */ - #[Assert\NotBlank()] public string $title; /** * The tracking number given by the Gateway. */ - #[Assert\NotBlank()] public string $value; - - public static function tryFrom($value): Tracking - { - if ($value instanceof Tracking) { - return $value; - } - - $tracking = new Tracking(); - $tracking->title = $value['title']; - $tracking->value = $value['value']; - - return $tracking; - } } diff --git a/src/Mapping/Provider/TrackingMapProvider.php b/src/Mapping/Provider/TrackingMapProvider.php new file mode 100644 index 0000000..acb431a --- /dev/null +++ b/src/Mapping/Provider/TrackingMapProvider.php @@ -0,0 +1,25 @@ +trackingRepository->findOneBy([ + 'title' => $source->title, + 'value' => $source->value + ]); + } +} diff --git a/src/Repository/Gateway/TrackingRepository.php b/src/Repository/Gateway/TrackingRepository.php new file mode 100644 index 0000000..8b21f42 --- /dev/null +++ b/src/Repository/Gateway/TrackingRepository.php @@ -0,0 +1,43 @@ + + */ +class TrackingRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Tracking::class); + } + +// /** +// * @return Tracking[] Returns an array of Tracking objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('t.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Tracking +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} From d9f2fd242bea5b6a5b1858ba28e36a0c1afdbd51 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 22 May 2025 13:24:26 +0200 Subject: [PATCH 076/203] Update Checkout Tracking relationship --- .../Gateway/CheckoutApiResource.php | 7 --- src/Benzina/CheckoutsPump.php | 18 ++++---- src/Entity/Gateway/Checkout.php | 45 +++++++----------- src/Entity/Gateway/Tracking.php | 2 +- src/Gateway/Paypal/PaypalGateway.php | 31 ++++++------- src/Gateway/Stripe/StripeGateway.php | 28 ++++------- src/Mapping/Provider/TrackingMapProvider.php | 2 +- src/Repository/Gateway/TrackingRepository.php | 46 +++++++++---------- 8 files changed, 73 insertions(+), 106 deletions(-) diff --git a/src/ApiResource/Gateway/CheckoutApiResource.php b/src/ApiResource/Gateway/CheckoutApiResource.php index 7e904a5..8f55637 100644 --- a/src/ApiResource/Gateway/CheckoutApiResource.php +++ b/src/ApiResource/Gateway/CheckoutApiResource.php @@ -106,8 +106,6 @@ class CheckoutApiResource * @var Tracking[] */ #[API\ApiProperty(writable: false)] - #[MapTo(Checkout::class, transformer: 'parseTrackings')] - #[MapFrom(Checkout::class, transformer: 'parseTrackings')] public array $trackings = []; public \DateTimeInterface $dateCreated; @@ -117,9 +115,4 @@ class CheckoutApiResource { return \array_map(fn($value) => Link::tryFrom($value), $values); } - - public static function parseTrackings(array $values) - { - return \array_map(fn($value) => Tracking::tryFrom($value), $values); - } } diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index 010a92a..4b226f9 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -4,6 +4,7 @@ namespace App\Benzina; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\Tracking; use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Tipjar; @@ -15,7 +16,6 @@ use App\Gateway\Gateway\CecaGateway; use App\Gateway\Gateway\DropGateway; use App\Gateway\Paypal\PaypalGateway; use App\Gateway\Stripe\StripeGateway; -use App\Gateway\Tracking; use App\Gateway\Wallet\WalletGateway; use App\Repository\Project\ProjectRepository; use App\Repository\TipjarRepository; @@ -214,31 +214,31 @@ class CheckoutsPump implements PumpInterface private function getCheckoutTrackings(array $record): array { $v3Tracking = new Tracking(); - $v3Tracking->title = self::TRACKING_TITLE_V3; - $v3Tracking->value = $record['id']; + $v3Tracking->setTitle(self::TRACKING_TITLE_V3); + $v3Tracking->setValue($record['id']); $trackings = [$v3Tracking]; if (!empty($record['payment'])) { $payment = new Tracking(); - $payment->title = self::TRACKING_TITLE_PAYMENT; - $payment->value = $record['payment']; + $payment->setTitle(self::TRACKING_TITLE_PAYMENT); + $payment->setValue($record['payment']); $trackings[] = $payment; } if (!empty($record['transaction'])) { $transaction = new Tracking(); - $transaction->title = self::TRACKING_TITLE_TRANSACTION; - $transaction->value = $record['transaction']; + $transaction->setTitle(self::TRACKING_TITLE_TRANSACTION); + $transaction->setValue($record['transaction']); $trackings[] = $transaction; } if (!empty($record['preapproval'])) { $preapproval = new Tracking(); - $preapproval->title = self::TRACKING_TITLE_PREAPPROVAL; - $preapproval->value = $record['preapproval']; + $preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL); + $preapproval->setValue($record['preapproval']); $trackings[] = $preapproval; } diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index de40774..4a35d8b 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -9,7 +9,6 @@ use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\RefundStrategy; -use App\Gateway\Tracking; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Gateway\CheckoutRepository; use App\Validator\SupportedChargeTypes; @@ -99,18 +98,16 @@ class Checkout private array $links = []; /** - * A list of tracking codes provided by the Gateway for this checkout.\ - * e.g: Order ID, Payment Capture ID, Checkout Session Token. - * - * @var Tracking[] + * @var Collection */ - #[ORM\Column] - private array $trackings = []; + #[ORM\OneToMany(targetEntity: Tracking::class, mappedBy: 'checkout', cascade: ['persist'])] + private Collection $trackings; public function __construct() { $this->status = CheckoutStatus::InPending; $this->charges = new ArrayCollection(); + $this->trackings = new ArrayCollection(); } public function getId(): ?int @@ -268,41 +265,31 @@ class Checkout } /** - * @return Tracking[] + * @return Collection */ - public function getTrackings(): array + public function getTrackings(): Collection { - return array_map(fn($t) => Tracking::tryFrom($t), $this->trackings); - } - - /** - * @param Tracking[] $trackings - */ - public function setTrackings(array $trackings): static - { - $this->trackings = $trackings; - - return $this; + return $this->trackings; } public function addTracking(Tracking $tracking): static { - $this->removeTracking($tracking); - - $this->trackings = [...$this->trackings, $tracking]; + if (!$this->trackings->contains($tracking)) { + $this->trackings->add($tracking); + $tracking->setCheckout($this); + } return $this; } public function removeTracking(Tracking $tracking): static { - $this->trackings = \array_filter( - \array_map(fn($t) => Tracking::tryFrom($t), $this->trackings), - function (Tracking $existingTracking) use ($tracking) { - return $existingTracking->title !== $tracking->title - && $existingTracking->value !== $tracking->value; + if ($this->trackings->removeElement($tracking)) { + // set the owning side to null (unless already changed) + if ($tracking->getCheckout() === $this) { + $tracking->setCheckout(null); } - ); + } return $this; } diff --git a/src/Entity/Gateway/Tracking.php b/src/Entity/Gateway/Tracking.php index 51229d6..733b63b 100644 --- a/src/Entity/Gateway/Tracking.php +++ b/src/Entity/Gateway/Tracking.php @@ -29,7 +29,7 @@ class Tracking public function __construct( ?string $title = null, - ?string $value = null + ?string $value = null, ) { $this->title = $title ?? $title; $this->value = $value ?? $value; diff --git a/src/Gateway/Paypal/PaypalGateway.php b/src/Gateway/Paypal/PaypalGateway.php index 4c07538..3d44132 100644 --- a/src/Gateway/Paypal/PaypalGateway.php +++ b/src/Gateway/Paypal/PaypalGateway.php @@ -4,12 +4,12 @@ namespace App\Gateway\Paypal; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\Tracking; use App\Gateway\AbstractGateway; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\LinkType; -use App\Gateway\Tracking; use Brick\Money\Money as BrickMoney; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -64,9 +64,7 @@ class PaypalGateway extends AbstractGateway 'payment_source' => $this->getPaypalPaymentSource($checkout), ]); - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_ORDER; - $tracking->value = $order['id']; + $tracking = new Tracking(self::TRACKING_TITLE_ORDER, $order['id']); $checkout->addTracking($tracking); @@ -111,9 +109,10 @@ class PaypalGateway extends AbstractGateway } foreach ($capture['purchase_units'] as $purchaseUnit) { - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_TRANSACTION; - $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + $tracking = new Tracking( + self::TRACKING_TITLE_TRANSACTION, + $purchaseUnit['payments']['captures'][0]['id'] + ); $checkout->addTracking($tracking); } @@ -189,9 +188,10 @@ class PaypalGateway extends AbstractGateway } foreach ($event['resource']['purchase_units'] as $purchaseUnit) { - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_TRANSACTION; - $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + $tracking = new Tracking( + self::TRACKING_TITLE_TRANSACTION, + $purchaseUnit['payments']['captures'][0]['id'] + ); $checkout->addTracking($tracking); } @@ -263,16 +263,15 @@ class PaypalGateway extends AbstractGateway public function processRefund(Charge $charge): void { $trackings = $charge->getCheckout()->getTrackings(); - $captureTracking = current(array_filter( - $trackings, - fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_TRANSACTION && $tracking->value - )); + $captureTracking = $trackings->filter(function (Tracking $t) { + return $t->getTitle() === self::TRACKING_TITLE_TRANSACTION && $t->getValue(); + })->current(); if (!$captureTracking) { - throw new \Exception('PayPal capture ID not found in tracking.'); + throw new \Exception('Tracking for PayPal capture ID not found.'); } - $captureId = $captureTracking->value; + $captureId = $captureTracking->getValue(); $response = $this->paypal->refundCapture($captureId, [ 'amount' => $this->getPaypalMoney($charge), diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 31b0d4f..96a7198 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -4,6 +4,7 @@ namespace App\Gateway\Stripe; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\Tracking; use App\Entity\Project\Project; use App\Entity\User\User; use App\Gateway\AbstractGateway; @@ -11,7 +12,6 @@ use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\LinkType; -use App\Gateway\Tracking; use App\Service\Gateway\CheckoutService; use Stripe\Checkout\Session as StripeSession; use Stripe\StripeClient; @@ -71,12 +71,7 @@ class StripeGateway extends AbstractGateway $link->type = LinkType::Payment; $checkout->addLink($link); - - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_CHECKOUT; - $tracking->value = $session->id; - - $checkout->addTracking($tracking); + $checkout->addTracking(new Tracking(self::TRACKING_TITLE_CHECKOUT, $session->id)); return $checkout; } @@ -209,22 +204,15 @@ class StripeGateway extends AbstractGateway public function processRefund(Charge $charge): void { $trackings = $charge->getCheckout()->getTrackings(); - $filteredTrackings = array_filter( - $trackings, - fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_CHECKOUT - ); + $sessionTracking = $trackings->filter(function (Tracking $t) { + return $t->getTitle() === self::TRACKING_TITLE_CHECKOUT; + })->current(); - $firstTracking = $filteredTrackings[0] ?? null; - - if (!$firstTracking || $firstTracking->title !== self::TRACKING_TITLE_CHECKOUT) { - throw new \Exception('Tracking not found or not related to Stripe checkout.'); + if (!$sessionTracking) { + throw new \Exception('Tracking for Stripe Session ID not found.'); } - $sessionId = $firstTracking->value; - - if (!$sessionId) { - throw new \Exception('Session ID not found for the charge.'); - } + $sessionId = $sessionTracking->getValue(); $session = $this->stripe->checkout->sessions->retrieve($sessionId); diff --git a/src/Mapping/Provider/TrackingMapProvider.php b/src/Mapping/Provider/TrackingMapProvider.php index acb431a..34b4548 100644 --- a/src/Mapping/Provider/TrackingMapProvider.php +++ b/src/Mapping/Provider/TrackingMapProvider.php @@ -19,7 +19,7 @@ class TrackingMapProvider implements ProviderInterface { return $this->trackingRepository->findOneBy([ 'title' => $source->title, - 'value' => $source->value + 'value' => $source->value, ]); } } diff --git a/src/Repository/Gateway/TrackingRepository.php b/src/Repository/Gateway/TrackingRepository.php index 8b21f42..89af268 100644 --- a/src/Repository/Gateway/TrackingRepository.php +++ b/src/Repository/Gateway/TrackingRepository.php @@ -16,28 +16,28 @@ class TrackingRepository extends ServiceEntityRepository parent::__construct($registry, Tracking::class); } -// /** -// * @return Tracking[] Returns an array of Tracking objects -// */ -// public function findByExampleField($value): array -// { -// return $this->createQueryBuilder('t') -// ->andWhere('t.exampleField = :val') -// ->setParameter('val', $value) -// ->orderBy('t.id', 'ASC') -// ->setMaxResults(10) -// ->getQuery() -// ->getResult() -// ; -// } + // /** + // * @return Tracking[] Returns an array of Tracking objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } -// public function findOneBySomeField($value): ?Tracking -// { -// return $this->createQueryBuilder('t') -// ->andWhere('t.exampleField = :val') -// ->setParameter('val', $value) -// ->getQuery() -// ->getOneOrNullResult() -// ; -// } + // public function findOneBySomeField($value): ?Tracking + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } } From 1a514852a0c77a144374540c6b1481c30fa47b03 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 22 May 2025 13:28:03 +0200 Subject: [PATCH 077/203] Allow to filter charges by checkout trackings --- src/ApiResource/Gateway/ChargeApiResource.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index 8c535f1..dc3b4f1 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -38,6 +38,7 @@ class ChargeApiResource * The Checkout to which this Charge item belongs to. */ #[API\ApiProperty(writable: false, security: 'is_granted("IS_AUTHENTICATED_FULLY")')] + #[API\ApiFilter(Filter\SearchFilter::class, properties: ['checkout.trackings.value'])] public CheckoutApiResource $checkout; /** From c8f7e86aae2db7f1800e80c35b56b694bfed9802 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 22 May 2025 15:20:15 +0200 Subject: [PATCH 078/203] Allow to filter charges by checkout gateway --- src/ApiResource/Gateway/ChargeApiResource.php | 2 + src/Filter/GatewayFilter.php | 99 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/Filter/GatewayFilter.php diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index dc3b4f1..eafe108 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -9,6 +9,7 @@ use App\ApiResource\Accounting\AccountingApiResource; use App\Dto\Gateway\ChargeUpdationDto; use App\Entity\Gateway\Charge; use App\Entity\Money; +use App\Filter\GatewayFilter; use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\State\ApiResourceStateProvider; @@ -38,6 +39,7 @@ class ChargeApiResource * The Checkout to which this Charge item belongs to. */ #[API\ApiProperty(writable: false, security: 'is_granted("IS_AUTHENTICATED_FULLY")')] + #[API\ApiFilter(GatewayFilter::class, properties: ['checkout.gateway'])] #[API\ApiFilter(Filter\SearchFilter::class, properties: ['checkout.trackings.value'])] public CheckoutApiResource $checkout; diff --git a/src/Filter/GatewayFilter.php b/src/Filter/GatewayFilter.php new file mode 100644 index 0000000..4a62d8d --- /dev/null +++ b/src/Filter/GatewayFilter.php @@ -0,0 +1,99 @@ +isPropertyEnabled($property, $resourceClass) + ) { + return; + } + + $values = (array) $value; + $rootAlias = $queryBuilder->getRootAliases()[0]; + $parameterName = ':'.$queryNameGenerator->generateParameterName($property); + + if ($this->isPropertyNested($property, $resourceClass)) { + [$alias] = $this->addJoinsForNestedProperty($property, $rootAlias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN); + } + + $aliasedField = \sprintf('%s.gatewayName', $alias); + + if (\count($values) === 1) { + $queryBuilder + ->andWhere($queryBuilder->expr()->eq($aliasedField, $parameterName)) + ->setParameter($parameterName, $this->getGatewayName($values[0])); + + return; + } + + $queryBuilder + ->andWhere($queryBuilder->expr()->in($aliasedField, $parameterName)) + ->setParameter($parameterName, array_map(fn($v) => $this->getGatewayName($v), $values)); + + return; + } + + private function getGatewayName(mixed $value): string + { + /** @var GatewayApiResource */ + $gateway = $this->iriConverter->getResourceFromIri($value); + + return $gateway->name; + } + + public function getDescription(string $resourceClass): array + { + $description = []; + + foreach ($this->properties as $property => $strategy) { + $description[$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'is_collection' => false, + ]; + + $description[$property.'[]'] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_ARRAY, + 'required' => false, + 'is_collection' => true, + ]; + } + + return $description; + } +} From 7a051505e90bfadc6c0c907433edf1e3e95982ea Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 28 May 2025 15:43:18 +0200 Subject: [PATCH 079/203] Update default values for match strategies --- src/Entity/Matchfunding/MatchStrategy.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 0316f7d..58049cf 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -4,6 +4,7 @@ namespace App\Entity\Matchfunding; use App\Entity\Money; use App\Mapping\Provider\MatchStrategyMapProvider; +use App\Matchfunding\Formula\MultiplicationFormula; use App\Repository\Matchfunding\MatchStrategyRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\DBAL\Types\Types; @@ -22,22 +23,30 @@ class MatchStrategy private array $ruleNames = []; #[ORM\Column(length: 255, nullable: true)] - private ?string $formulaName = null; + private ?string $formulaName; #[ORM\Embedded(class: Money::class)] private ?object $limit = null; #[ORM\Column(nullable: true)] - private ?float $factor = null; + private ?float $factor = 1.0; #[ORM\Column(enumType: MatchAgainst::class)] private ?MatchAgainst $against = MatchAgainst::DEFAULT; - public static function of(MatchCall $call): MatchStrategy - { + public static function of( + MatchCall $call, + ?string $formulaName = null, + ): MatchStrategy { $strategy = new MatchStrategy(); $strategy->setCall($call); + $strategy->setFormulaName($formulaName ?? MultiplicationFormula::getName()); + $strategy->setLimit(new Money( + 2147483647, + $call->getAccounting()->getCurrency() + )); + return $strategy; } From 6d6096b8657b8f7fe9287d1cc5fce3cd553b4717 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 28 May 2025 16:02:13 +0200 Subject: [PATCH 080/203] Add status property to MatchCall --- .../Matchfunding/MatchCallApiResource.php | 6 ++++ src/Entity/Matchfunding/MatchCall.php | 15 +++++++++ src/Entity/Matchfunding/MatchCallStatus.php | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/Entity/Matchfunding/MatchCallStatus.php diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index ebb11cf..de461ac 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -7,6 +7,7 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\Matchfunding\MatchCall; +use App\Entity\Matchfunding\MatchCallStatus; use App\Entity\Territory; use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; @@ -82,4 +83,9 @@ class MatchCallApiResource #[Assert\NotBlank()] #[Assert\Valid] public Territory $territory; + + /** + * The current status of the MatchCall. + */ + public MatchCallStatus $status; } diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index b1ed62e..e707786 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -51,6 +51,9 @@ class MatchCall implements AccountingOwnerInterface #[ORM\Embedded(class: Territory::class)] private ?Territory $territory = null; + #[ORM\Column(enumType: MatchCallStatus::class)] + private ?MatchCallStatus $status = MatchCallStatus::InEditing; + public function __construct() { $this->accounting = Accounting::of($this); @@ -182,4 +185,16 @@ class MatchCall implements AccountingOwnerInterface return $this; } + + public function getStatus(): ?MatchCallStatus + { + return $this->status; + } + + public function setStatus(MatchCallStatus $status): static + { + $this->status = $status; + + return $this; + } } diff --git a/src/Entity/Matchfunding/MatchCallStatus.php b/src/Entity/Matchfunding/MatchCallStatus.php new file mode 100644 index 0000000..491f39f --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallStatus.php @@ -0,0 +1,31 @@ + Date: Thu, 29 May 2025 14:51:23 +0200 Subject: [PATCH 081/203] Validate against new submissions for calls not in_calling status --- src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php index 9299538..b07c337 100644 --- a/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php +++ b/src/Dto/Matchfunding/MatchCallSubmissionCreationDto.php @@ -14,6 +14,10 @@ class MatchCallSubmissionCreationDto * The MatchCall to which this MatchCallSubmission belongs to. */ #[Assert\NotBlank()] + #[Assert\Expression( + expression: 'value.status === enum("App\\\Entity\\\Matchfunding\\\MatchCallStatus::InCalling")', + message: 'MatchCallSubmissions can only relate to a MatchCall with `in_calling` status.' + )] public MatchCallApiResource $call; /** From fcb7211e8246fc92694914e0305681e7d2ae958a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 29 May 2025 14:57:53 +0200 Subject: [PATCH 082/203] Default status for MatchCallApiResource is in_editing --- src/ApiResource/Matchfunding/MatchCallApiResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index de461ac..8f4863b 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -87,5 +87,5 @@ class MatchCallApiResource /** * The current status of the MatchCall. */ - public MatchCallStatus $status; + public MatchCallStatus $status = MatchCallStatus::InEditing; } From 45f66d758cc70eab33dd3704ddc18fb06ae17e52 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 29 May 2025 14:58:06 +0200 Subject: [PATCH 083/203] Update to be closed status name --- src/Entity/Matchfunding/MatchCallStatus.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Matchfunding/MatchCallStatus.php b/src/Entity/Matchfunding/MatchCallStatus.php index 491f39f..6d29c74 100644 --- a/src/Entity/Matchfunding/MatchCallStatus.php +++ b/src/Entity/Matchfunding/MatchCallStatus.php @@ -22,7 +22,7 @@ enum MatchCallStatus: string /** * The MatchCall finished giving funds and closing actions can now be performed. */ - case InClosing = 'in_closing'; + case ToClosed = 'to_closed'; /** * The MatchCall is now fully finished and will remain as read-only. From 73aea8a3ff52b62e110a6e9d23161ea906ccbd56 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 2 Jun 2025 14:03:20 +0200 Subject: [PATCH 084/203] Bugfix, allow null user in project voter canEdit argument type --- src/Security/Voter/ProjectVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Security/Voter/ProjectVoter.php b/src/Security/Voter/ProjectVoter.php index 61c3889..3aaa5d6 100644 --- a/src/Security/Voter/ProjectVoter.php +++ b/src/Security/Voter/ProjectVoter.php @@ -37,7 +37,7 @@ final class ProjectVoter extends Voter return false; } - private function canEdit(ProjectApiResource $project, User $user): bool + private function canEdit(ProjectApiResource $project, ?User $user): bool { if (!$user instanceof User) { return false; From d539874b8fc83239445e85fa1c0cfb02534da7bc Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 2 Jun 2025 14:19:15 +0200 Subject: [PATCH 085/203] Update continue level to skip to next matchcall submission when one rule does not meet --- src/Service/Matchfunding/MatchfundingService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index 97159b8..55c9d8e 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -42,7 +42,7 @@ class MatchfundingService foreach ($this->ruleLocator->getFrom($strategy) as $rule) { if (!$rule->validate($charge, $submission)) { - continue; + continue 2; } } From 5596ed076c73fc6b2ac09aad6d1e7c39cd0faaa5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 2 Jun 2025 14:54:51 +0200 Subject: [PATCH 086/203] Update Project test cases --- tests/Entity/ProjectApi/CreateTest.php | 2 +- tests/Entity/ProjectApi/DeleteTest.php | 4 ++-- tests/Entity/ProjectApi/GetAllTest.php | 2 +- tests/Entity/ProjectApi/GetOneTest.php | 2 +- tests/Entity/ProjectApi/{BaseTest.php => ProjectTestCase.php} | 2 +- tests/Entity/ProjectApi/UpdateTest.php | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) rename tests/Entity/ProjectApi/{BaseTest.php => ProjectTestCase.php} (98%) diff --git a/tests/Entity/ProjectApi/CreateTest.php b/tests/Entity/ProjectApi/CreateTest.php index 183c347..a6aff83 100644 --- a/tests/Entity/ProjectApi/CreateTest.php +++ b/tests/Entity/ProjectApi/CreateTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class CreateTest extends BaseTest +class CreateTest extends ProjectTestCase { // Auxiliary functions diff --git a/tests/Entity/ProjectApi/DeleteTest.php b/tests/Entity/ProjectApi/DeleteTest.php index f0b13af..f016fa9 100644 --- a/tests/Entity/ProjectApi/DeleteTest.php +++ b/tests/Entity/ProjectApi/DeleteTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class DeleteTest extends BaseTest +class DeleteTest extends ProjectTestCase { protected function getMethod(): string { @@ -29,7 +29,7 @@ class DeleteTest extends BaseTest static::createClient()->request('DELETE', $this->getUri(1)); - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); } public function testDeleteWithInvalidToken(): void diff --git a/tests/Entity/ProjectApi/GetAllTest.php b/tests/Entity/ProjectApi/GetAllTest.php index 4a72d40..ade088c 100644 --- a/tests/Entity/ProjectApi/GetAllTest.php +++ b/tests/Entity/ProjectApi/GetAllTest.php @@ -8,7 +8,7 @@ use App\Entity\Territory; use App\Factory\Project\ProjectFactory; use Symfony\Component\HttpFoundation\Response; -class GetAllTest extends BaseTest +class GetAllTest extends ProjectTestCase { private const PAGE_SIZE = 30; diff --git a/tests/Entity/ProjectApi/GetOneTest.php b/tests/Entity/ProjectApi/GetOneTest.php index 1130ae3..afcf93c 100644 --- a/tests/Entity/ProjectApi/GetOneTest.php +++ b/tests/Entity/ProjectApi/GetOneTest.php @@ -5,7 +5,7 @@ namespace App\Tests\Entity\ProjectApi; use App\Entity\Project\Project; use App\Entity\Project\ProjectStatus; -class GetOneTest extends BaseTest +class GetOneTest extends ProjectTestCase { // Auxiliary functions diff --git a/tests/Entity/ProjectApi/BaseTest.php b/tests/Entity/ProjectApi/ProjectTestCase.php similarity index 98% rename from tests/Entity/ProjectApi/BaseTest.php rename to tests/Entity/ProjectApi/ProjectTestCase.php index a79d385..b76f2ab 100644 --- a/tests/Entity/ProjectApi/BaseTest.php +++ b/tests/Entity/ProjectApi/ProjectTestCase.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -abstract class BaseTest extends ApiTestCase +abstract class ProjectTestCase extends ApiTestCase { use ResetDatabase; use Factories; diff --git a/tests/Entity/ProjectApi/UpdateTest.php b/tests/Entity/ProjectApi/UpdateTest.php index 4943a5a..4304fee 100644 --- a/tests/Entity/ProjectApi/UpdateTest.php +++ b/tests/Entity/ProjectApi/UpdateTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class UpdateTest extends BaseTest +class UpdateTest extends ProjectTestCase { // Auxiliary functions @@ -33,7 +33,7 @@ class UpdateTest extends BaseTest static::createClient()->request($this->getMethod(), $this->getUri(1)); - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); } public function testUpdateWithInvalidToken(): void From 859732493c3496466b7b304d88149cbe6dd3cf59 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 9 Jun 2025 11:25:52 +0200 Subject: [PATCH 087/203] Limit max items per page for clients to 100 --- config/packages/api_platform.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 93a3cf4..446bc45 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -32,7 +32,7 @@ api_platform: standard_put: true pagination_enabled: true pagination_items_per_page: 30 - pagination_client_enabled: true + pagination_maximum_items_per_page: 100 pagination_client_items_per_page: true use_symfony_listeners: true swagger: From adfde98a4ea0a241526633ec58c20ccf46d44381 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 16 Jun 2025 11:28:25 +0200 Subject: [PATCH 088/203] Update match call<->strategy relationship --- .../Matchfunding/MatchCallApiResource.php | 6 ++-- .../Matchfunding/MatchStrategyApiResource.php | 21 +++++++----- src/Entity/Matchfunding/MatchCall.php | 34 ++++++++++++++----- src/Entity/Matchfunding/MatchStrategy.php | 15 ++++++-- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 8f4863b..2078b9f 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -46,10 +46,12 @@ class MatchCallApiResource public AccountingApiResource $accounting; /** - * The MatchStrategy defines the match behaviour for this MatchCall. + * The MatchStrategies define the matching behaviour for this MatchCall. + * + * @var MatchStrategyApiResource[] */ #[API\ApiProperty(writable: false)] - public MatchStrategyApiResource $strategy; + public array $strategies; /** * A list of the MatchCallSubmissions received by this MatchCall. diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index c5356d3..55a2e3f 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Matchfunding; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Money; @@ -30,20 +31,22 @@ use Symfony\Component\Validator\Constraints as Assert; stateOptions: new Options(entityClass: MatchStrategy::class), provider: ApiResourceStateProvider::class, processor: ApiResourceStateProcessor::class, - uriTemplate: '/match_call/{id}/strategy', - uriVariables: [ - 'id' => new API\Link( - fromClass: MatchCallApiResource::class, - fromProperty: 'strategy', - description: 'MatchCall identifier' - ), - ] )] +#[API\GetCollection()] +#[API\Post()] #[API\Get()] -#[API\Patch()] +#[API\Patch(security: 'is_granted("MATCHCALL_EDIT", object.call)')] +#[API\Delete(security: 'is_granted("MATCHCALL_EDIT", object.call)')] class MatchStrategyApiResource { #[API\ApiProperty(identifier: true, writable: false)] + public int $id; + + /** + * The MatchCall to which this strategy belongs to. + */ + #[Assert\NotBlank()] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public MatchCallApiResource $call; /** diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index e707786..0075020 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -27,8 +27,11 @@ class MatchCall implements AccountingOwnerInterface #[ORM\JoinColumn(nullable: false)] private ?Accounting $accounting = null; - #[ORM\OneToOne(mappedBy: 'call', cascade: ['persist', 'remove'])] - private ?MatchStrategy $strategy = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: MatchStrategy::class, mappedBy: 'call')] + private Collection $strategies; /** * @var Collection @@ -57,9 +60,9 @@ class MatchCall implements AccountingOwnerInterface public function __construct() { $this->accounting = Accounting::of($this); - $this->strategy = MatchStrategy::of($this); $this->managers = new ArrayCollection(); $this->submissions = new ArrayCollection(); + $this->strategies = new ArrayCollection(); } public function getId(): ?int @@ -79,19 +82,32 @@ class MatchCall implements AccountingOwnerInterface return $this; } - public function getStrategy(): ?MatchStrategy + /** + * @return Collection + */ + public function getStrategies(): Collection { - return $this->strategy; + return $this->strategies; } - public function setStrategy(MatchStrategy $strategy): static + public function addStrategy(MatchStrategy $strategy): static { - // set the owning side of the relation if necessary - if ($strategy->getCall() !== $this) { + if (!$this->strategies->contains($strategy)) { + $this->strategies->add($strategy); $strategy->setCall($this); } - $this->strategy = $strategy; + return $this; + } + + public function removeStrategy(MatchStrategy $strategy): static + { + if ($this->strategies->removeElement($strategy)) { + // set the owning side to null (unless already changed) + if ($strategy->getCall() === $this) { + $strategy->setCall(null); + } + } return $this; } diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 58049cf..10cab8a 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -15,7 +15,11 @@ use Doctrine\ORM\Mapping as ORM; class MatchStrategy { #[ORM\Id] - #[ORM\OneToOne(inversedBy: 'strategy', cascade: ['persist', 'remove'])] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'strategies')] #[ORM\JoinColumn(nullable: false)] private ?MatchCall $call = null; @@ -39,8 +43,8 @@ class MatchStrategy ?string $formulaName = null, ): MatchStrategy { $strategy = new MatchStrategy(); - $strategy->setCall($call); + $strategy->setCall($call); $strategy->setFormulaName($formulaName ?? MultiplicationFormula::getName()); $strategy->setLimit(new Money( 2147483647, @@ -50,12 +54,17 @@ class MatchStrategy return $strategy; } + public function getId(): ?int + { + return $this->id; + } + public function getCall(): ?MatchCall { return $this->call; } - public function setCall(MatchCall $call): static + public function setCall(?MatchCall $call): static { $this->call = $call; From 62c4bf17caae175161caa3d54058e493db4f9ac8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 16 Jun 2025 12:09:29 +0200 Subject: [PATCH 089/203] Process first validated MatchStrategy --- .../Matchfunding/MatchCallApiResource.php | 2 +- .../Matchfunding/MatchfundingService.php | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php index 2078b9f..1048333 100644 --- a/src/ApiResource/Matchfunding/MatchCallApiResource.php +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -47,7 +47,7 @@ class MatchCallApiResource /** * The MatchStrategies define the matching behaviour for this MatchCall. - * + * * @var MatchStrategyApiResource[] */ #[API\ApiProperty(writable: false)] diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index 55c9d8e..d9ef507 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -38,30 +38,32 @@ class MatchfundingService $transactions = []; foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { - $strategy = $submission->getCall()->getStrategy(); - - foreach ($this->ruleLocator->getFrom($strategy) as $rule) { - if (!$rule->validate($charge, $submission)) { - continue 2; + foreach ($submission->getCall()->getStrategies() as $strategy) { + foreach ($this->ruleLocator->getFrom($strategy) as $rule) { + if (!$rule->validate($charge, $submission)) { + continue 2; + } } + + $toBeMatched = match ($strategy->getAgainst()) { + MatchAgainst::Charge => $charge->getMoney(), + MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum->value], + MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum->value], + }; + + $matched = $this->formulaLocator + ->get($strategy->getFormulaName()) + ->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit()); + + $transaction = new Transaction(); + $transaction->setMoney($matched); + $transaction->setOrigin($submission->getCall()->getAccounting()); + $transaction->setTarget($submission->getProject()->getAccounting()); + + $transactions[] = $transaction; + + continue; } - - $toBeMatched = match ($strategy->getAgainst()) { - MatchAgainst::Charge => $charge->getMoney(), - MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum->value], - MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum->value], - }; - - $matched = $this->formulaLocator - ->get($strategy->getFormulaName()) - ->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit()); - - $transaction = new Transaction(); - $transaction->setMoney($matched); - $transaction->setOrigin($submission->getCall()->getAccounting()); - $transaction->setTarget($submission->getProject()->getAccounting()); - - $transactions[] = $transaction; } return $transactions; From 716f7c3c8fe414e1979c04847a3d0e09a7331706 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 16 Jun 2025 13:58:23 +0200 Subject: [PATCH 090/203] Add ranking to MatchStrategies --- .../Matchfunding/MatchStrategyApiResource.php | 14 ++++- src/Entity/Matchfunding/MatchCall.php | 10 ++++ src/Entity/Matchfunding/MatchStrategy.php | 15 +++++ .../MatchStrategyStateProcessor.php | 59 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/State/Matchfunding/MatchStrategyStateProcessor.php diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index 55a2e3f..bcd3f8a 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -21,9 +21,9 @@ use Symfony\Component\Validator\Constraints as Assert; * \ * The match-making flow is: * 1. A Project accepted in a MatchCall receives a successful Charge. - * 2. The match-making loads the MatchCall's MatchStrategy. - * 3. The MatchStrategy rules are executed, if one rule fails the match-making is cancelled for the Charge. - * 3. The MatchStrategy formula function is passed the respective limit, factor and money of the MatchAgainst. + * 2. The match-making loads the MatchCall's MatchStrategies. + * 3. For each MatchStrategy the rules are executed, if one rule fails the match-making is cancelled for the strategy. + * 3. The first valid MatchStrategy's formula function is passed the respective limit, factor and money of the MatchAgainst. * 4. The result of the MatchStrategy formula function execution is put in a Transaction from the MatchCall to the Project. */ #[API\ApiResource( @@ -49,6 +49,14 @@ class MatchStrategyApiResource #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public MatchCallApiResource $call; + /** + * The ranking is the index order of this strategy among the others of the MatchCall + * when this is up for match making. The ranking value of items will be sorted + * after each change in the number of strategies or the ranking values. + */ + #[Assert\PositiveOrZero()] + public int $ranking = 0; + /** * The MatchRules used to decide if the match making strategy should be executed or not. * diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 0075020..2f27563 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -90,6 +90,16 @@ class MatchCall implements AccountingOwnerInterface return $this->strategies; } + /** + * @param Collection $strategies + */ + public function setStrategies(Collection $strategies): static + { + $this->strategies = $strategies; + + return $this; + } + public function addStrategy(MatchStrategy $strategy): static { if (!$this->strategies->contains($strategy)) { diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 10cab8a..5b1cfa0 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -23,6 +23,9 @@ class MatchStrategy #[ORM\JoinColumn(nullable: false)] private ?MatchCall $call = null; + #[ORM\Column] + private ?int $ranking = null; + #[ORM\Column(type: Types::ARRAY)] private array $ruleNames = []; @@ -71,6 +74,18 @@ class MatchStrategy return $this; } + public function getRanking(): ?int + { + return $this->ranking; + } + + public function setRanking(int $ranking): static + { + $this->ranking = $ranking; + + return $this; + } + public function getRuleNames(): array { return $this->ruleNames; diff --git a/src/State/Matchfunding/MatchStrategyStateProcessor.php b/src/State/Matchfunding/MatchStrategyStateProcessor.php new file mode 100644 index 0000000..852c0a1 --- /dev/null +++ b/src/State/Matchfunding/MatchStrategyStateProcessor.php @@ -0,0 +1,59 @@ +autoMapper->map($data, MatchStrategy::class); + $call = $strategy->getCall(); + + $strategies = $call->getStrategies()->filter(fn(MatchStrategy $s) => $s->getId() === $strategy->getId()); + $strategies = $strategies->map(function (MatchStrategy $item) use ($strategy, $operation) { + $iRanking = $item->getRanking(); + $sRanking = $strategy->getRanking(); + + if ($iRanking < $sRanking) { + return $item; + } + + if ($iRanking >= $sRanking) { + $newRanking = $operation instanceof DeleteOperationInterface ? $iRanking - 1 : $iRanking + 1; + + return $item->setRanking($newRanking); + } + }); + $strategies->add($strategy); + + $call->setStrategies($strategies); + + $this->entityManager->persist($call); + $this->entityManager->flush(); + + $strategy = $this->entityProcessor->process($strategy, $operation, $uriVariables, $context); + + return $this->autoMapper->map($strategy, MatchStrategyApiResource::class); + } +} From 261db39b24fadda37f8ec7eac43bcc43c798a60b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 17 Jun 2025 11:00:42 +0200 Subject: [PATCH 091/203] Add captain-definition file --- captain-definition.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 captain-definition.json diff --git a/captain-definition.json b/captain-definition.json new file mode 100644 index 0000000..b8b9dd6 --- /dev/null +++ b/captain-definition.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 2, + "dockerfilePath": "./docker/php/Dockerfile" +} From 2d0360b69b0f80833be719651a9a4b0594672bd6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 17 Jun 2025 11:06:55 +0200 Subject: [PATCH 092/203] Remove file extension from captain-definition file --- captain-definition.json => captain-definition | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename captain-definition.json => captain-definition (100%) diff --git a/captain-definition.json b/captain-definition similarity index 100% rename from captain-definition.json rename to captain-definition From 5f9d5c276b11718edace8c6c778edb13fd9ba0c6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Jun 2025 13:48:35 +0200 Subject: [PATCH 093/203] Remove specific map provider for match strategy --- src/Entity/Matchfunding/MatchStrategy.php | 4 ++-- .../Provider/MatchStrategyMapProvider.php | 22 ------------------- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 src/Mapping/Provider/MatchStrategyMapProvider.php diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 5b1cfa0..8f9b002 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -3,14 +3,14 @@ namespace App\Entity\Matchfunding; use App\Entity\Money; -use App\Mapping\Provider\MatchStrategyMapProvider; +use App\Mapping\Provider\EntityMapProvider; use App\Matchfunding\Formula\MultiplicationFormula; use App\Repository\Matchfunding\MatchStrategyRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -#[MapProvider(MatchStrategyMapProvider::class)] +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: MatchStrategyRepository::class)] class MatchStrategy { diff --git a/src/Mapping/Provider/MatchStrategyMapProvider.php b/src/Mapping/Provider/MatchStrategyMapProvider.php deleted file mode 100644 index 5841ced..0000000 --- a/src/Mapping/Provider/MatchStrategyMapProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -matchStrategyRepository->find($source->call->id); - } -} From 95621494344cce4f5c9bef65d1e31706c28f4f38 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Jun 2025 13:49:13 +0200 Subject: [PATCH 094/203] Sort match strategies and update rankings on call changes --- src/Entity/Matchfunding/MatchCall.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Entity/Matchfunding/MatchCall.php b/src/Entity/Matchfunding/MatchCall.php index 2f27563..0868f91 100644 --- a/src/Entity/Matchfunding/MatchCall.php +++ b/src/Entity/Matchfunding/MatchCall.php @@ -30,7 +30,7 @@ class MatchCall implements AccountingOwnerInterface /** * @var Collection */ - #[ORM\OneToMany(targetEntity: MatchStrategy::class, mappedBy: 'call')] + #[ORM\OneToMany(targetEntity: MatchStrategy::class, mappedBy: 'call', cascade: ['persist'])] private Collection $strategies; /** @@ -103,7 +103,16 @@ class MatchCall implements AccountingOwnerInterface public function addStrategy(MatchStrategy $strategy): static { if (!$this->strategies->contains($strategy)) { + $this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) { + if ($s->getRanking() >= $strategy->getRanking()) { + $s->setRanking($s->getRanking() + 1); + } + + return $s; + }); + $this->strategies->add($strategy); + $strategy->setCall($this); } @@ -113,10 +122,13 @@ class MatchCall implements AccountingOwnerInterface public function removeStrategy(MatchStrategy $strategy): static { if ($this->strategies->removeElement($strategy)) { - // set the owning side to null (unless already changed) - if ($strategy->getCall() === $this) { - $strategy->setCall(null); - } + $this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) { + if ($s->getRanking() > $strategy->getRanking()) { + $s->setRanking($s->getRanking() - 1); + } + + return $s; + }); } return $this; From 3e5dbcac40ca315c2ac7c472af8484a8677dccaa Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 18 Jun 2025 13:49:38 +0200 Subject: [PATCH 095/203] Pass match strategies via dedicated state processor --- .../Matchfunding/MatchStrategyApiResource.php | 6 ++-- .../MatchStrategyStateProcessor.php | 28 +++++++------------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index bcd3f8a..c13485a 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Matchfunding; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; @@ -10,8 +11,8 @@ use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchStrategy; use App\Mapping\Transformer\MatchFormulaMapTransformer; use App\Mapping\Transformer\MatchRulesMapTransformer; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use App\State\Matchfunding\MatchStrategyStateProcessor; use AutoMapper\Attribute\MapFrom; use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; @@ -30,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert; shortName: 'MatchStrategy', stateOptions: new Options(entityClass: MatchStrategy::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class, + processor: MatchStrategyStateProcessor::class, )] #[API\GetCollection()] #[API\Post()] @@ -55,6 +56,7 @@ class MatchStrategyApiResource * after each change in the number of strategies or the ranking values. */ #[Assert\PositiveOrZero()] + #[API\ApiFilter(OrderFilter::class)] public int $ranking = 0; /** diff --git a/src/State/Matchfunding/MatchStrategyStateProcessor.php b/src/State/Matchfunding/MatchStrategyStateProcessor.php index 852c0a1..da7a94d 100644 --- a/src/State/Matchfunding/MatchStrategyStateProcessor.php +++ b/src/State/Matchfunding/MatchStrategyStateProcessor.php @@ -28,32 +28,24 @@ class MatchStrategyStateProcessor implements ProcessorInterface { /** @var MatchStrategy */ $strategy = $this->autoMapper->map($data, MatchStrategy::class); + $call = $strategy->getCall(); - $strategies = $call->getStrategies()->filter(fn(MatchStrategy $s) => $s->getId() === $strategy->getId()); - $strategies = $strategies->map(function (MatchStrategy $item) use ($strategy, $operation) { - $iRanking = $item->getRanking(); - $sRanking = $strategy->getRanking(); - - if ($iRanking < $sRanking) { - return $item; - } - - if ($iRanking >= $sRanking) { - $newRanking = $operation instanceof DeleteOperationInterface ? $iRanking - 1 : $iRanking + 1; - - return $item->setRanking($newRanking); - } - }); - $strategies->add($strategy); - - $call->setStrategies($strategies); + if ($operation instanceof DeleteOperationInterface) { + $call->removeStrategy($strategy); + } else { + $call->addStrategy($strategy); + } $this->entityManager->persist($call); $this->entityManager->flush(); $strategy = $this->entityProcessor->process($strategy, $operation, $uriVariables, $context); + if ($strategy === null) { + return null; + } + return $this->autoMapper->map($strategy, MatchStrategyApiResource::class); } } From 17832fa3177d9d08567729c52ab1644d7086a7b1 Mon Sep 17 00:00:00 2001 From: djoven89 Date: Thu, 19 Jun 2025 16:08:31 +0200 Subject: [PATCH 096/203] Several changes to allow the deployment in other environments --- .dockerignore | 3 +- .github/workflows/build.yaml | 2 +- captain-definition | 2 +- composer.json | 8 +- composer.lock | 774 ++++++++++---------- docker-compose.override.yml | 6 +- docker-compose.yml | 8 +- docker/{ => dev}/nginx/nginx.conf | 0 docker/{ => dev}/php/Dockerfile | 2 +- docker/{ => dev}/php/conf.d/opcache.dev.ini | 0 docker/{ => dev}/php/conf.d/opcache.ini | 0 docker/{ => dev}/php/conf.d/openssl.ini | 0 docker/{ => dev}/php/conf.d/xdebug.dev.ini | 0 docker/{ => dev}/php/entrypoint.sh | 0 docker/{ => dev}/php/ssl/.gitignore | 0 docker/main/Dockerfile | 53 ++ docker/main/default.conf | 33 + docker/main/entrypoint.sh | 19 + docker/main/supervisord.conf | 26 + 19 files changed, 536 insertions(+), 400 deletions(-) rename docker/{ => dev}/nginx/nginx.conf (100%) rename docker/{ => dev}/php/Dockerfile (94%) rename docker/{ => dev}/php/conf.d/opcache.dev.ini (100%) rename docker/{ => dev}/php/conf.d/opcache.ini (100%) rename docker/{ => dev}/php/conf.d/openssl.ini (100%) rename docker/{ => dev}/php/conf.d/xdebug.dev.ini (100%) rename docker/{ => dev}/php/entrypoint.sh (100%) rename docker/{ => dev}/php/ssl/.gitignore (100%) create mode 100644 docker/main/Dockerfile create mode 100644 docker/main/default.conf create mode 100644 docker/main/entrypoint.sh create mode 100644 docker/main/supervisord.conf diff --git a/.dockerignore b/.dockerignore index ec5b9bd..6c1c0ff 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ /public/bundles/ -/var/ +var/ +vendor/ .phpunit.result.cache /phpunit.xml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0a3fb51..66219dc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: with: push: true context: . - file: docker/php/Dockerfile + file: docker/dev/php/Dockerfile target: prod tags: ${{ secrets.REGISTRY_URL }}/goteo-v4:latest diff --git a/captain-definition b/captain-definition index b8b9dd6..414182a 100644 --- a/captain-definition +++ b/captain-definition @@ -1,4 +1,4 @@ { "schemaVersion": 2, - "dockerfilePath": "./docker/php/Dockerfile" + "dockerfilePath": "./docker/main/Dockerfile" } diff --git a/composer.json b/composer.json index 2b4a63f..28a8e03 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "symfony/framework-bundle": "6.4.*", "symfony/http-client": "6.4.*", "symfony/intl": "6.4.*", + "symfony/maker-bundle": "^1.62", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", "symfony/property-info": "6.4.*", @@ -44,7 +45,8 @@ "symfony/serializer": "6.4.*", "symfony/twig-bundle": "6.4.*", "symfony/validator": "6.4.*", - "symfony/yaml": "6.4.*" + "symfony/yaml": "6.4.*", + "zenstruck/foundry": "^2.4" }, "config": { "allow-plugins": { @@ -99,8 +101,6 @@ "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", - "symfony/maker-bundle": "^1.62", - "symfony/phpunit-bridge": "^7.2", - "zenstruck/foundry": "^2.4" + "symfony/phpunit-bridge": "^7.2" } } diff --git a/composer.lock b/composer.lock index 9526179..18ccbb0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6066bdd681ac8f5f7b3d2d8ae4e72149", + "content-hash": "f049d2e637e75cf873dddba113d9a2ab", "packages": [ { "name": "api-platform/doctrine-common", @@ -2876,6 +2876,69 @@ }, "time": "2024-12-14T10:54:02+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, { "name": "gedmo/doctrine-extensions", "version": "v3.20.0", @@ -6476,6 +6539,98 @@ ], "time": "2024-10-25T15:19:46+00:00" }, + { + "name": "symfony/maker-bundle", + "version": "v1.63.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-26T01:41:37+00:00" + }, { "name": "symfony/monolog-bridge", "version": "v6.4.13", @@ -7179,6 +7334,67 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/process", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-10T17:11:00+00:00" + }, { "name": "symfony/property-access", "version": "v6.4.18", @@ -9213,6 +9429,175 @@ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, "time": "2022-01-30T20:08:53+00:00" + }, + { + "name": "zenstruck/assert", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/assert.git", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5.21", + "symfony/phpunit-bridge": "^6.3|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Standalone, lightweight, framework agnostic, test assertion library.", + "homepage": "https://github.com/zenstruck/assert", + "keywords": [ + "assertion", + "phpunit", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/assert/issues", + "source": "https://github.com/zenstruck/assert/tree/v1.5.1" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2024-10-28T18:08:12+00:00" + }, + { + "name": "zenstruck/foundry", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/foundry.git", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/830d5a5243b5b81e79b2b91b67654836490ed833", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.23", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", + "zenstruck/assert": "^1.4" + }, + "conflict": { + "doctrine/persistence": "<2.0", + "symfony/framework-bundle": "<6.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", + "brianium/paratest": "^6|^7", + "dama/doctrine-test-bundle": "^8.0", + "doctrine/collections": "^1.7|^2.0", + "doctrine/common": "^3.2.2", + "doctrine/doctrine-bundle": "^2.10", + "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", + "doctrine/mongodb-odm": "^2.4", + "doctrine/mongodb-odm-bundle": "^4.6|^5.0", + "doctrine/orm": "^2.16|^3.0", + "doctrine/persistence": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/maker-bundle": "^1.55", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/runtime": "^6.4|^7.0", + "symfony/translation-contracts": "^3.4", + "symfony/uid": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" + }, + "bamarni-bin": { + "bin-links": true, + "forward-command": false, + "target-directory": "bin/tools" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/Persistence/functions.php", + "src/symfony_console.php" + ], + "psr-4": { + "Zenstruck\\Foundry\\": "src/", + "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + }, + { + "name": "Nicolas PHILIPPE", + "email": "nikophil@gmail.com" + } + ], + "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", + "homepage": "https://github.com/zenstruck/foundry", + "keywords": [ + "Fixture", + "dev", + "doctrine", + "factory", + "faker", + "symfony", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/foundry/issues", + "source": "https://github.com/zenstruck/foundry/tree/v2.6.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2025-06-05T08:34:41+00:00" } ], "packages-dev": [ @@ -9472,69 +9857,6 @@ }, "time": "2023-08-08T05:53:35+00:00" }, - { - "name": "fakerphp/faker", - "version": "v1.24.1", - "source": { - "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." - }, - "type": "library", - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" - }, - "time": "2024-11-21T13:46:39+00:00" - }, { "name": "fidry/cpu-core-counter", "version": "1.2.0", @@ -12056,98 +12378,6 @@ ], "time": "2025-02-14T17:58:34+00:00" }, - { - "name": "symfony/maker-bundle", - "version": "v1.63.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/maker-bundle.git", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425", - "shasum": "" - }, - "require": { - "doctrine/inflector": "^2.0", - "nikic/php-parser": "^5.0", - "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" - }, - "conflict": { - "doctrine/doctrine-bundle": "<2.10", - "doctrine/orm": "<2.15" - }, - "require-dev": { - "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.0|^4.x-dev" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Bundle\\MakerBundle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", - "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", - "keywords": [ - "code generator", - "dev", - "generator", - "scaffold", - "scaffolding" - ], - "support": { - "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-26T01:41:37+00:00" - }, { "name": "symfony/options-resolver", "version": "v6.4.16", @@ -12297,67 +12527,6 @@ ], "time": "2025-04-09T08:35:42+00:00" }, - { - "name": "symfony/process", - "version": "v6.4.20", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v6.4.20" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-03-10T17:11:00+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -12407,176 +12576,11 @@ } ], "time": "2024-03-03T12:36:25+00:00" - }, - { - "name": "zenstruck/assert", - "version": "v1.5.1", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/assert.git", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277", - "shasum": "" - }, - "require": { - "php": ">=8.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5.4|^6.0|^7.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^9.5.21", - "symfony/phpunit-bridge": "^6.3|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Zenstruck\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - } - ], - "description": "Standalone, lightweight, framework agnostic, test assertion library.", - "homepage": "https://github.com/zenstruck/assert", - "keywords": [ - "assertion", - "phpunit", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/assert/issues", - "source": "https://github.com/zenstruck/assert/tree/v1.5.1" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2024-10-28T18:08:12+00:00" - }, - { - "name": "zenstruck/foundry", - "version": "v2.4.2", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/foundry.git", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/277472cc5f938fd98c2a5cf0a317b8283d897be6", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0|^3.0|^4.0", - "fakerphp/faker": "^1.23", - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.2|^3.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", - "zenstruck/assert": "^1.4" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "brianium/paratest": "^6|^7", - "dama/doctrine-test-bundle": "^8.0", - "doctrine/collections": "^1.7|^2.0", - "doctrine/common": "^3.2.2", - "doctrine/doctrine-bundle": "^2.10", - "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", - "doctrine/mongodb-odm": "^2.4", - "doctrine/mongodb-odm-bundle": "^4.6|^5.0", - "doctrine/orm": "^2.16|^3.0", - "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", - "symfony/console": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/maker-bundle": "^1.55", - "symfony/phpunit-bridge": "^6.4|^7.0", - "symfony/runtime": "^6.4|^7.0", - "symfony/translation-contracts": "^3.4", - "symfony/uid": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" - }, - "type": "library", - "extra": { - "psalm": { - "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" - }, - "bamarni-bin": { - "bin-links": true, - "forward-command": false, - "target-directory": "bin/tools" - } - }, - "autoload": { - "files": [ - "src/functions.php", - "src/Persistence/functions.php", - "src/symfony_console.php" - ], - "psr-4": { - "Zenstruck\\Foundry\\": "src/", - "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - }, - { - "name": "Nicolas PHILIPPE", - "email": "nikophil@gmail.com" - } - ], - "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", - "homepage": "https://github.com/zenstruck/foundry", - "keywords": [ - "Fixture", - "dev", - "doctrine", - "factory", - "faker", - "symfony", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v2.4.2" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2025-04-17T06:26:28+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -12584,6 +12588,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a776fbc..b06cbb2 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,9 +1,9 @@ services: php: volumes: - # ./docker/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro - - ./docker/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro - - ./docker/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro + # ./docker/dev/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro + - ./docker/dev/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro extra_hosts: - "host.docker.internal:host-gateway" user: "${UID:-1000}:${GID:-1000}" diff --git a/docker-compose.yml b/docker-compose.yml index dcaac30..344ab10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,20 +2,20 @@ services: php: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/dev/php/Dockerfile target: dev container_name: goteo-api-php volumes: - .:/app - - ./docker/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro - - ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro + - ./docker/dev/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro nginx: image: nginx:alpine container_name: goteo-api-nginx volumes: - .:/app - - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./docker/dev/nginx/nginx.conf:/etc/nginx/conf.d/default.conf ports: - "${APP_HTTP_PORT:-8090}:80" - "${APP_HTTPS_PORT:-8091}:443" diff --git a/docker/nginx/nginx.conf b/docker/dev/nginx/nginx.conf similarity index 100% rename from docker/nginx/nginx.conf rename to docker/dev/nginx/nginx.conf diff --git a/docker/php/Dockerfile b/docker/dev/php/Dockerfile similarity index 94% rename from docker/php/Dockerfile rename to docker/dev/php/Dockerfile index dd8ab34..f33d406 100644 --- a/docker/php/Dockerfile +++ b/docker/dev/php/Dockerfile @@ -47,7 +47,7 @@ FROM base AS prod COPY . /app RUN chown -R www-data:www-data /app -COPY /docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY /docker/dev/php/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh USER www-data diff --git a/docker/php/conf.d/opcache.dev.ini b/docker/dev/php/conf.d/opcache.dev.ini similarity index 100% rename from docker/php/conf.d/opcache.dev.ini rename to docker/dev/php/conf.d/opcache.dev.ini diff --git a/docker/php/conf.d/opcache.ini b/docker/dev/php/conf.d/opcache.ini similarity index 100% rename from docker/php/conf.d/opcache.ini rename to docker/dev/php/conf.d/opcache.ini diff --git a/docker/php/conf.d/openssl.ini b/docker/dev/php/conf.d/openssl.ini similarity index 100% rename from docker/php/conf.d/openssl.ini rename to docker/dev/php/conf.d/openssl.ini diff --git a/docker/php/conf.d/xdebug.dev.ini b/docker/dev/php/conf.d/xdebug.dev.ini similarity index 100% rename from docker/php/conf.d/xdebug.dev.ini rename to docker/dev/php/conf.d/xdebug.dev.ini diff --git a/docker/php/entrypoint.sh b/docker/dev/php/entrypoint.sh similarity index 100% rename from docker/php/entrypoint.sh rename to docker/dev/php/entrypoint.sh diff --git a/docker/php/ssl/.gitignore b/docker/dev/php/ssl/.gitignore similarity index 100% rename from docker/php/ssl/.gitignore rename to docker/dev/php/ssl/.gitignore diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile new file mode 100644 index 0000000..890055d --- /dev/null +++ b/docker/main/Dockerfile @@ -0,0 +1,53 @@ +FROM php:8.3-fpm-alpine AS php-system-setup + +RUN apk --no-cache add \ + curl \ + git \ + linux-headers \ + icu-dev \ + zip \ + libzip-dev \ + unzip + +RUN docker-php-ext-install \ + pdo \ + pdo_mysql \ + intl \ + zip + +RUN docker-php-ext-install opcache + +RUN apk --no-cache add $PHPIZE_DEPS && \ + pecl install apcu \ + && docker-php-ext-enable apcu + +RUN curl --remote-name https://curl.se/ca/cacert.pem \ + && mv cacert.pem /etc/ssl/certs/ca-certificates.crt + +COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer + +FROM php-system-setup AS webserver-setup + +RUN apk add --update --no-cache \ + nginx \ + supervisor + +COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf +COPY ./docker/main/supervisord.conf /etc/supervisord.conf + +FROM webserver-setup AS app-setup + +COPY . /app +RUN chown -R nginx:nginx /app + +COPY ./docker/main/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER nginx +WORKDIR /app + +RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader + +EXPOSE 80 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/main/default.conf b/docker/main/default.conf new file mode 100644 index 0000000..a1814f4 --- /dev/null +++ b/docker/main/default.conf @@ -0,0 +1,33 @@ +server { + + listen 80; + index index.php; + server_name localhost; + root /app/public; + + error_log /dev/stdout; + access_log /dev/stdout; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ ^/index\.php(/|$) { + fastcgi_pass localhost:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + + internal; + } + + location ~ \.php$ { + return 404; + } +} diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh new file mode 100644 index 0000000..9d81720 --- /dev/null +++ b/docker/main/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +# Check if migrations need to be applied +if ! php bin/console doctrine:migrations:up-to-date --quiet; then + echo "Applying migrations..." + php bin/console doctrine:migrations:migrate --no-interaction +fi + +# Check if FORCE_SCHEMA_UPDATE is set to true +if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then + echo "Forcing schema update..." + php bin/console doctrine:schema:update --force +fi + +echo "Start Supervisord" + +supervisord -c /etc/supervisord.conf diff --git a/docker/main/supervisord.conf b/docker/main/supervisord.conf new file mode 100644 index 0000000..2492f86 --- /dev/null +++ b/docker/main/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +logfile=/dev/null +logfile_maxbytes=0 +logfile_backups=0 +loglevel=info +nodaemon=true + +[program:php-fpm] +command=php-fpm --nodaemonize +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:nginx] +command=nginx -g 'daemon off;' +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr From 9c4e6585f4e3b221227f0bfeaf93c7ce9adb120b Mon Sep 17 00:00:00 2001 From: djoven89 Date: Thu, 19 Jun 2025 16:52:14 +0200 Subject: [PATCH 097/203] Add a basic health check --- docker/main/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 890055d..acaa6dd 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -50,4 +50,7 @@ RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader EXPOSE 80 +HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ + CMD curl -s -o /dev/null -f http://localhost/v4/index.json || exit 1 + ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] From 7aa14f5983551e3cd8485bc601530e73c12ae40e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 20 Jun 2025 13:42:56 +0200 Subject: [PATCH 098/203] Process match calls by ranking --- .../Matchfunding/MatchStrategyRepository.php | 28 +++++++++---------- .../Matchfunding/MatchfundingService.php | 7 ++++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Repository/Matchfunding/MatchStrategyRepository.php b/src/Repository/Matchfunding/MatchStrategyRepository.php index e86c34c..d4e9cbe 100644 --- a/src/Repository/Matchfunding/MatchStrategyRepository.php +++ b/src/Repository/Matchfunding/MatchStrategyRepository.php @@ -2,6 +2,7 @@ namespace App\Repository\Matchfunding; +use App\Entity\Matchfunding\MatchCall; use App\Entity\Matchfunding\MatchStrategy; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -16,20 +17,19 @@ class MatchStrategyRepository extends ServiceEntityRepository parent::__construct($registry, MatchStrategy::class); } - // /** - // * @return MatchStrategy[] Returns an array of MatchStrategy 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() - // ; - // } + /** + * @return MatchStrategy[] Returns an array of ranked MatchStrategy objects + */ + public function findByCall(MatchCall $call): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.call = :val') + ->setParameter('val', $call) + ->orderBy('m.ranking', 'ASC') + ->getQuery() + ->getResult() + ; + } // public function findOneBySomeField($value): ?MatchStrategy // { diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php index d9ef507..5424906 100644 --- a/src/Service/Matchfunding/MatchfundingService.php +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -10,6 +10,7 @@ use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Matchfunding\Formula\FormulaLocator; use App\Matchfunding\Rule\RuleLocator; +use App\Repository\Matchfunding\MatchStrategyRepository; use App\Service\Project\BudgetService; class MatchfundingService @@ -20,6 +21,7 @@ class MatchfundingService private RuleLocator $ruleLocator, private FormulaLocator $formulaLocator, private BudgetService $budgetService, + private MatchStrategyRepository $matchStrategyRepository, ) {} /** @@ -38,9 +40,12 @@ class MatchfundingService $transactions = []; foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { - foreach ($submission->getCall()->getStrategies() as $strategy) { + $rankedStrategies = $this->matchStrategyRepository->findByCall($submission->getCall()); + + foreach ($rankedStrategies as $strategy) { foreach ($this->ruleLocator->getFrom($strategy) as $rule) { if (!$rule->validate($charge, $submission)) { + // If an strategy's rule fails, skip to the next strategy continue 2; } } From a55607562bcb87187e6eb2d9cee3827faa6fc031 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 23 Jun 2025 13:33:17 +0200 Subject: [PATCH 099/203] Fix missing money in support money transformer --- src/Mapping/Transformer/SupportMoneyMapTransformer.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php index 0aac7b7..a123a25 100644 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ b/src/Mapping/Transformer/SupportMoneyMapTransformer.php @@ -18,12 +18,11 @@ class SupportMoneyMapTransformer implements PropertyTransformerInterface */ public function transform(mixed $value, object|array $source, array $context): mixed { + $money = new Money(0, $source->getProject()->getAccounting()->getCurrency()); + $charges = $source->getCharges(); foreach ($charges as $charge) { - $money = $this->moneyService->add( - $charge->getMoney(), - $money ?? new Money(0, $charge->getMoney()->currency) - ); + $money = $this->moneyService->add($charge->getMoney(), $money); } return $money; From 68fcf2e641bd289bd066fbae4864d78c4ca0848c Mon Sep 17 00:00:00 2001 From: djoven89 Date: Tue, 24 Jun 2025 11:28:30 +0200 Subject: [PATCH 100/203] Improvements in Docker --- .dockerignore | 10 ++++---- docker/main/Dockerfile | 52 +++++++++++++++++++++++++--------------- docker/main/default.conf | 4 ++-- docker/main/local.ini | 9 +++++++ docker/main/www.conf | 18 ++++++++++++++ 5 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 docker/main/local.ini create mode 100644 docker/main/www.conf diff --git a/.dockerignore b/.dockerignore index 6c1c0ff..b0ea767 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,13 @@ -/public/bundles/ +public/bundles/ var/ vendor/ .phpunit.result.cache -/phpunit.xml +phpunit.xml -/phpunit.xml -.phpunit.result.cache +.php-cs-fixer.php +.php-cs-fixer.cache -/.php-cs-fixer.php -/.php-cs-fixer.cache .git/ .github/ .vscode/ diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index acaa6dd..0cf0932 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.3-fpm-alpine AS php-system-setup +FROM php:8.3-fpm-alpine AS system-setup RUN apk --no-cache add \ curl \ @@ -7,35 +7,49 @@ RUN apk --no-cache add \ icu-dev \ zip \ libzip-dev \ - unzip + unzip \ + nginx \ + supervisor \ + $PHPIZE_DEPS && \ + pecl install apcu + +RUN curl --remote-name https://curl.se/ca/cacert.pem \ + && mv cacert.pem /etc/ssl/certs/ca-certificates.crt RUN docker-php-ext-install \ pdo \ pdo_mysql \ intl \ - zip - -RUN docker-php-ext-install opcache - -RUN apk --no-cache add $PHPIZE_DEPS && \ - pecl install apcu \ - && docker-php-ext-enable apcu - -RUN curl --remote-name https://curl.se/ca/cacert.pem \ - && mv cacert.pem /etc/ssl/certs/ca-certificates.crt + zip \ + opcache && \ + docker-php-ext-enable apcu COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer -FROM php-system-setup AS webserver-setup - -RUN apk add --update --no-cache \ - nginx \ - supervisor - COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf COPY ./docker/main/supervisord.conf /etc/supervisord.conf +COPY ./docker/main/www.conf /usr/local/etc/php-fpm.d/www.conf +COPY ./docker/main/local.ini /usr/local/etc/php/conf.d/local.ini -FROM webserver-setup AS app-setup +# FPM configuration via ENV +ENV FPM_PM=dynamic +ENV FPM_PM_MAX_CHILDREN=5 +ENV FPM_PM_START_SERVERS=2 +ENV FPM_PM_MIN_SPARE_SERVERS=1 +ENV FPM_PM_MAX_SPARE_SERVERS=3 + +# PHP configuration via ENV +ENV LOG_TO_STDOUT=1 +ENV PHP_MEMORY_LIMIT=512M +ENV PHP_MAX_EXECUTION_TIME=30 +ENV PHP_UPLOAD_MAX_FILESIZE=120M +ENV PHP_POST_MAX_SIZE=128M +ENV PHP_SESSION_SAVE_HANDLER=files +ENV PHP_SESSION_SAVE_PATH= +ENV PHP_DISPLAY_ERRORS=0 +ENV PHP_DISPLAY_STARTUP_ERRORS=0 + +FROM system-setup AS app-setup COPY . /app RUN chown -R nginx:nginx /app diff --git a/docker/main/default.conf b/docker/main/default.conf index a1814f4..8f99d47 100644 --- a/docker/main/default.conf +++ b/docker/main/default.conf @@ -1,10 +1,10 @@ server { - listen 80; - index index.php; server_name localhost; root /app/public; + index index.php; + error_log /dev/stdout; access_log /dev/stdout; diff --git a/docker/main/local.ini b/docker/main/local.ini new file mode 100644 index 0000000..00adf04 --- /dev/null +++ b/docker/main/local.ini @@ -0,0 +1,9 @@ +memory_limit = ${PHP_MEMORY_LIMIT} +max_execution_time = ${PHP_MAX_EXECUTION_TIME} +upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE} +post_max_size = ${PHP_POST_MAX_SIZE} +allow_url_fopen = 1 +session.save_handler = ${PHP_SESSION_SAVE_HANDLER} +session.save_path = ${PHP_SESSION_SAVE_PATH} +display_errors = ${PHP_DISPLAY_ERRORS} +display_startup_errors = ${PHP_DISPLAY_STARTUP_ERRORS} diff --git a/docker/main/www.conf b/docker/main/www.conf new file mode 100644 index 0000000..0d45d03 --- /dev/null +++ b/docker/main/www.conf @@ -0,0 +1,18 @@ +[global] +daemonize = no + +[www] +user = nginx +group = nginx +listen = 9000 +listen.owner = nginx +listen.group = nginx +listen.mode = 0660 + +pm = ${FPM_PM} +pm.max_children = ${FPM_PM_MAX_CHILDREN} +pm.start_servers = ${FPM_PM_START_SERVERS} +pm.min_spare_servers = ${FPM_PM_MIN_SPARE_SERVERS} +pm.max_spare_servers = ${FPM_PM_MAX_SPARE_SERVERS} + +catch_workers_output = yes From c7fe3af24a45958b3b0b35ddfcf2019803fbaa67 Mon Sep 17 00:00:00 2001 From: djoven89 Date: Wed, 25 Jun 2025 10:52:01 +0200 Subject: [PATCH 101/203] Add cache warm command to the entrypoint --- docker/main/entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh index 9d81720..5421176 100644 --- a/docker/main/entrypoint.sh +++ b/docker/main/entrypoint.sh @@ -14,6 +14,8 @@ if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then php bin/console doctrine:schema:update --force fi +php bin/console cache:warmup + echo "Start Supervisord" supervisord -c /etc/supervisord.conf From 516c6ac47b29b7e604a0e8eaf6eb473ee56d2974 Mon Sep 17 00:00:00 2001 From: djoven89 Date: Thu, 19 Jun 2025 16:08:31 +0200 Subject: [PATCH 102/203] Several changes to allow the deployment in other environments --- .dockerignore | 3 +- .github/workflows/build.yaml | 2 +- captain-definition | 2 +- composer.json | 8 +- composer.lock | 774 ++++++++++---------- docker-compose.override.yml | 6 +- docker-compose.yml | 8 +- docker/{ => dev}/nginx/nginx.conf | 0 docker/{ => dev}/php/Dockerfile | 2 +- docker/{ => dev}/php/conf.d/opcache.dev.ini | 0 docker/{ => dev}/php/conf.d/opcache.ini | 0 docker/{ => dev}/php/conf.d/openssl.ini | 0 docker/{ => dev}/php/conf.d/xdebug.dev.ini | 0 docker/{ => dev}/php/entrypoint.sh | 0 docker/{ => dev}/php/ssl/.gitignore | 0 docker/main/Dockerfile | 53 ++ docker/main/default.conf | 33 + docker/main/entrypoint.sh | 19 + docker/main/supervisord.conf | 26 + 19 files changed, 536 insertions(+), 400 deletions(-) rename docker/{ => dev}/nginx/nginx.conf (100%) rename docker/{ => dev}/php/Dockerfile (94%) rename docker/{ => dev}/php/conf.d/opcache.dev.ini (100%) rename docker/{ => dev}/php/conf.d/opcache.ini (100%) rename docker/{ => dev}/php/conf.d/openssl.ini (100%) rename docker/{ => dev}/php/conf.d/xdebug.dev.ini (100%) rename docker/{ => dev}/php/entrypoint.sh (100%) rename docker/{ => dev}/php/ssl/.gitignore (100%) create mode 100644 docker/main/Dockerfile create mode 100644 docker/main/default.conf create mode 100644 docker/main/entrypoint.sh create mode 100644 docker/main/supervisord.conf diff --git a/.dockerignore b/.dockerignore index ec5b9bd..6c1c0ff 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ /public/bundles/ -/var/ +var/ +vendor/ .phpunit.result.cache /phpunit.xml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0a3fb51..66219dc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: with: push: true context: . - file: docker/php/Dockerfile + file: docker/dev/php/Dockerfile target: prod tags: ${{ secrets.REGISTRY_URL }}/goteo-v4:latest diff --git a/captain-definition b/captain-definition index b8b9dd6..414182a 100644 --- a/captain-definition +++ b/captain-definition @@ -1,4 +1,4 @@ { "schemaVersion": 2, - "dockerfilePath": "./docker/php/Dockerfile" + "dockerfilePath": "./docker/main/Dockerfile" } diff --git a/composer.json b/composer.json index 2b4a63f..28a8e03 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "symfony/framework-bundle": "6.4.*", "symfony/http-client": "6.4.*", "symfony/intl": "6.4.*", + "symfony/maker-bundle": "^1.62", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", "symfony/property-info": "6.4.*", @@ -44,7 +45,8 @@ "symfony/serializer": "6.4.*", "symfony/twig-bundle": "6.4.*", "symfony/validator": "6.4.*", - "symfony/yaml": "6.4.*" + "symfony/yaml": "6.4.*", + "zenstruck/foundry": "^2.4" }, "config": { "allow-plugins": { @@ -99,8 +101,6 @@ "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", - "symfony/maker-bundle": "^1.62", - "symfony/phpunit-bridge": "^7.2", - "zenstruck/foundry": "^2.4" + "symfony/phpunit-bridge": "^7.2" } } diff --git a/composer.lock b/composer.lock index 9526179..18ccbb0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6066bdd681ac8f5f7b3d2d8ae4e72149", + "content-hash": "f049d2e637e75cf873dddba113d9a2ab", "packages": [ { "name": "api-platform/doctrine-common", @@ -2876,6 +2876,69 @@ }, "time": "2024-12-14T10:54:02+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, { "name": "gedmo/doctrine-extensions", "version": "v3.20.0", @@ -6476,6 +6539,98 @@ ], "time": "2024-10-25T15:19:46+00:00" }, + { + "name": "symfony/maker-bundle", + "version": "v1.63.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-26T01:41:37+00:00" + }, { "name": "symfony/monolog-bridge", "version": "v6.4.13", @@ -7179,6 +7334,67 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/process", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-10T17:11:00+00:00" + }, { "name": "symfony/property-access", "version": "v6.4.18", @@ -9213,6 +9429,175 @@ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, "time": "2022-01-30T20:08:53+00:00" + }, + { + "name": "zenstruck/assert", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/assert.git", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5.21", + "symfony/phpunit-bridge": "^6.3|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Standalone, lightweight, framework agnostic, test assertion library.", + "homepage": "https://github.com/zenstruck/assert", + "keywords": [ + "assertion", + "phpunit", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/assert/issues", + "source": "https://github.com/zenstruck/assert/tree/v1.5.1" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2024-10-28T18:08:12+00:00" + }, + { + "name": "zenstruck/foundry", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/foundry.git", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/830d5a5243b5b81e79b2b91b67654836490ed833", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.23", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", + "zenstruck/assert": "^1.4" + }, + "conflict": { + "doctrine/persistence": "<2.0", + "symfony/framework-bundle": "<6.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", + "brianium/paratest": "^6|^7", + "dama/doctrine-test-bundle": "^8.0", + "doctrine/collections": "^1.7|^2.0", + "doctrine/common": "^3.2.2", + "doctrine/doctrine-bundle": "^2.10", + "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", + "doctrine/mongodb-odm": "^2.4", + "doctrine/mongodb-odm-bundle": "^4.6|^5.0", + "doctrine/orm": "^2.16|^3.0", + "doctrine/persistence": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/maker-bundle": "^1.55", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/runtime": "^6.4|^7.0", + "symfony/translation-contracts": "^3.4", + "symfony/uid": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" + }, + "bamarni-bin": { + "bin-links": true, + "forward-command": false, + "target-directory": "bin/tools" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/Persistence/functions.php", + "src/symfony_console.php" + ], + "psr-4": { + "Zenstruck\\Foundry\\": "src/", + "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + }, + { + "name": "Nicolas PHILIPPE", + "email": "nikophil@gmail.com" + } + ], + "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", + "homepage": "https://github.com/zenstruck/foundry", + "keywords": [ + "Fixture", + "dev", + "doctrine", + "factory", + "faker", + "symfony", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/foundry/issues", + "source": "https://github.com/zenstruck/foundry/tree/v2.6.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2025-06-05T08:34:41+00:00" } ], "packages-dev": [ @@ -9472,69 +9857,6 @@ }, "time": "2023-08-08T05:53:35+00:00" }, - { - "name": "fakerphp/faker", - "version": "v1.24.1", - "source": { - "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." - }, - "type": "library", - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" - }, - "time": "2024-11-21T13:46:39+00:00" - }, { "name": "fidry/cpu-core-counter", "version": "1.2.0", @@ -12056,98 +12378,6 @@ ], "time": "2025-02-14T17:58:34+00:00" }, - { - "name": "symfony/maker-bundle", - "version": "v1.63.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/maker-bundle.git", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425", - "shasum": "" - }, - "require": { - "doctrine/inflector": "^2.0", - "nikic/php-parser": "^5.0", - "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" - }, - "conflict": { - "doctrine/doctrine-bundle": "<2.10", - "doctrine/orm": "<2.15" - }, - "require-dev": { - "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.0|^4.x-dev" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Bundle\\MakerBundle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", - "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", - "keywords": [ - "code generator", - "dev", - "generator", - "scaffold", - "scaffolding" - ], - "support": { - "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-26T01:41:37+00:00" - }, { "name": "symfony/options-resolver", "version": "v6.4.16", @@ -12297,67 +12527,6 @@ ], "time": "2025-04-09T08:35:42+00:00" }, - { - "name": "symfony/process", - "version": "v6.4.20", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v6.4.20" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-03-10T17:11:00+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -12407,176 +12576,11 @@ } ], "time": "2024-03-03T12:36:25+00:00" - }, - { - "name": "zenstruck/assert", - "version": "v1.5.1", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/assert.git", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277", - "shasum": "" - }, - "require": { - "php": ">=8.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5.4|^6.0|^7.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^9.5.21", - "symfony/phpunit-bridge": "^6.3|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Zenstruck\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - } - ], - "description": "Standalone, lightweight, framework agnostic, test assertion library.", - "homepage": "https://github.com/zenstruck/assert", - "keywords": [ - "assertion", - "phpunit", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/assert/issues", - "source": "https://github.com/zenstruck/assert/tree/v1.5.1" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2024-10-28T18:08:12+00:00" - }, - { - "name": "zenstruck/foundry", - "version": "v2.4.2", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/foundry.git", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/277472cc5f938fd98c2a5cf0a317b8283d897be6", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0|^3.0|^4.0", - "fakerphp/faker": "^1.23", - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.2|^3.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", - "zenstruck/assert": "^1.4" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "brianium/paratest": "^6|^7", - "dama/doctrine-test-bundle": "^8.0", - "doctrine/collections": "^1.7|^2.0", - "doctrine/common": "^3.2.2", - "doctrine/doctrine-bundle": "^2.10", - "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", - "doctrine/mongodb-odm": "^2.4", - "doctrine/mongodb-odm-bundle": "^4.6|^5.0", - "doctrine/orm": "^2.16|^3.0", - "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", - "symfony/console": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/maker-bundle": "^1.55", - "symfony/phpunit-bridge": "^6.4|^7.0", - "symfony/runtime": "^6.4|^7.0", - "symfony/translation-contracts": "^3.4", - "symfony/uid": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" - }, - "type": "library", - "extra": { - "psalm": { - "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" - }, - "bamarni-bin": { - "bin-links": true, - "forward-command": false, - "target-directory": "bin/tools" - } - }, - "autoload": { - "files": [ - "src/functions.php", - "src/Persistence/functions.php", - "src/symfony_console.php" - ], - "psr-4": { - "Zenstruck\\Foundry\\": "src/", - "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - }, - { - "name": "Nicolas PHILIPPE", - "email": "nikophil@gmail.com" - } - ], - "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", - "homepage": "https://github.com/zenstruck/foundry", - "keywords": [ - "Fixture", - "dev", - "doctrine", - "factory", - "faker", - "symfony", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v2.4.2" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2025-04-17T06:26:28+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -12584,6 +12588,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a776fbc..b06cbb2 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,9 +1,9 @@ services: php: volumes: - # ./docker/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro - - ./docker/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro - - ./docker/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro + # ./docker/dev/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro + - ./docker/dev/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro extra_hosts: - "host.docker.internal:host-gateway" user: "${UID:-1000}:${GID:-1000}" diff --git a/docker-compose.yml b/docker-compose.yml index dcaac30..344ab10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,20 +2,20 @@ services: php: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/dev/php/Dockerfile target: dev container_name: goteo-api-php volumes: - .:/app - - ./docker/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro - - ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro + - ./docker/dev/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro nginx: image: nginx:alpine container_name: goteo-api-nginx volumes: - .:/app - - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./docker/dev/nginx/nginx.conf:/etc/nginx/conf.d/default.conf ports: - "${APP_HTTP_PORT:-8090}:80" - "${APP_HTTPS_PORT:-8091}:443" diff --git a/docker/nginx/nginx.conf b/docker/dev/nginx/nginx.conf similarity index 100% rename from docker/nginx/nginx.conf rename to docker/dev/nginx/nginx.conf diff --git a/docker/php/Dockerfile b/docker/dev/php/Dockerfile similarity index 94% rename from docker/php/Dockerfile rename to docker/dev/php/Dockerfile index dd8ab34..f33d406 100644 --- a/docker/php/Dockerfile +++ b/docker/dev/php/Dockerfile @@ -47,7 +47,7 @@ FROM base AS prod COPY . /app RUN chown -R www-data:www-data /app -COPY /docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY /docker/dev/php/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh USER www-data diff --git a/docker/php/conf.d/opcache.dev.ini b/docker/dev/php/conf.d/opcache.dev.ini similarity index 100% rename from docker/php/conf.d/opcache.dev.ini rename to docker/dev/php/conf.d/opcache.dev.ini diff --git a/docker/php/conf.d/opcache.ini b/docker/dev/php/conf.d/opcache.ini similarity index 100% rename from docker/php/conf.d/opcache.ini rename to docker/dev/php/conf.d/opcache.ini diff --git a/docker/php/conf.d/openssl.ini b/docker/dev/php/conf.d/openssl.ini similarity index 100% rename from docker/php/conf.d/openssl.ini rename to docker/dev/php/conf.d/openssl.ini diff --git a/docker/php/conf.d/xdebug.dev.ini b/docker/dev/php/conf.d/xdebug.dev.ini similarity index 100% rename from docker/php/conf.d/xdebug.dev.ini rename to docker/dev/php/conf.d/xdebug.dev.ini diff --git a/docker/php/entrypoint.sh b/docker/dev/php/entrypoint.sh similarity index 100% rename from docker/php/entrypoint.sh rename to docker/dev/php/entrypoint.sh diff --git a/docker/php/ssl/.gitignore b/docker/dev/php/ssl/.gitignore similarity index 100% rename from docker/php/ssl/.gitignore rename to docker/dev/php/ssl/.gitignore diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile new file mode 100644 index 0000000..890055d --- /dev/null +++ b/docker/main/Dockerfile @@ -0,0 +1,53 @@ +FROM php:8.3-fpm-alpine AS php-system-setup + +RUN apk --no-cache add \ + curl \ + git \ + linux-headers \ + icu-dev \ + zip \ + libzip-dev \ + unzip + +RUN docker-php-ext-install \ + pdo \ + pdo_mysql \ + intl \ + zip + +RUN docker-php-ext-install opcache + +RUN apk --no-cache add $PHPIZE_DEPS && \ + pecl install apcu \ + && docker-php-ext-enable apcu + +RUN curl --remote-name https://curl.se/ca/cacert.pem \ + && mv cacert.pem /etc/ssl/certs/ca-certificates.crt + +COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer + +FROM php-system-setup AS webserver-setup + +RUN apk add --update --no-cache \ + nginx \ + supervisor + +COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf +COPY ./docker/main/supervisord.conf /etc/supervisord.conf + +FROM webserver-setup AS app-setup + +COPY . /app +RUN chown -R nginx:nginx /app + +COPY ./docker/main/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER nginx +WORKDIR /app + +RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader + +EXPOSE 80 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/main/default.conf b/docker/main/default.conf new file mode 100644 index 0000000..a1814f4 --- /dev/null +++ b/docker/main/default.conf @@ -0,0 +1,33 @@ +server { + + listen 80; + index index.php; + server_name localhost; + root /app/public; + + error_log /dev/stdout; + access_log /dev/stdout; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ ^/index\.php(/|$) { + fastcgi_pass localhost:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + + internal; + } + + location ~ \.php$ { + return 404; + } +} diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh new file mode 100644 index 0000000..9d81720 --- /dev/null +++ b/docker/main/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +# Check if migrations need to be applied +if ! php bin/console doctrine:migrations:up-to-date --quiet; then + echo "Applying migrations..." + php bin/console doctrine:migrations:migrate --no-interaction +fi + +# Check if FORCE_SCHEMA_UPDATE is set to true +if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then + echo "Forcing schema update..." + php bin/console doctrine:schema:update --force +fi + +echo "Start Supervisord" + +supervisord -c /etc/supervisord.conf diff --git a/docker/main/supervisord.conf b/docker/main/supervisord.conf new file mode 100644 index 0000000..2492f86 --- /dev/null +++ b/docker/main/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +logfile=/dev/null +logfile_maxbytes=0 +logfile_backups=0 +loglevel=info +nodaemon=true + +[program:php-fpm] +command=php-fpm --nodaemonize +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:nginx] +command=nginx -g 'daemon off;' +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr From 5ec315a26f2a4d1abdf347ee91f3ef7997d98b4f Mon Sep 17 00:00:00 2001 From: djoven89 Date: Thu, 19 Jun 2025 16:52:14 +0200 Subject: [PATCH 103/203] Add a basic health check --- docker/main/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 890055d..acaa6dd 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -50,4 +50,7 @@ RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader EXPOSE 80 +HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ + CMD curl -s -o /dev/null -f http://localhost/v4/index.json || exit 1 + ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] From 9ffa65baa9c87f22be634e3d45393efce98876a7 Mon Sep 17 00:00:00 2001 From: djoven89 Date: Tue, 24 Jun 2025 11:28:30 +0200 Subject: [PATCH 104/203] Improvements in Docker --- .dockerignore | 10 ++++---- docker/main/Dockerfile | 52 +++++++++++++++++++++++++--------------- docker/main/default.conf | 4 ++-- docker/main/local.ini | 9 +++++++ docker/main/www.conf | 18 ++++++++++++++ 5 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 docker/main/local.ini create mode 100644 docker/main/www.conf diff --git a/.dockerignore b/.dockerignore index 6c1c0ff..b0ea767 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,13 @@ -/public/bundles/ +public/bundles/ var/ vendor/ .phpunit.result.cache -/phpunit.xml +phpunit.xml -/phpunit.xml -.phpunit.result.cache +.php-cs-fixer.php +.php-cs-fixer.cache -/.php-cs-fixer.php -/.php-cs-fixer.cache .git/ .github/ .vscode/ diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index acaa6dd..0cf0932 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.3-fpm-alpine AS php-system-setup +FROM php:8.3-fpm-alpine AS system-setup RUN apk --no-cache add \ curl \ @@ -7,35 +7,49 @@ RUN apk --no-cache add \ icu-dev \ zip \ libzip-dev \ - unzip + unzip \ + nginx \ + supervisor \ + $PHPIZE_DEPS && \ + pecl install apcu + +RUN curl --remote-name https://curl.se/ca/cacert.pem \ + && mv cacert.pem /etc/ssl/certs/ca-certificates.crt RUN docker-php-ext-install \ pdo \ pdo_mysql \ intl \ - zip - -RUN docker-php-ext-install opcache - -RUN apk --no-cache add $PHPIZE_DEPS && \ - pecl install apcu \ - && docker-php-ext-enable apcu - -RUN curl --remote-name https://curl.se/ca/cacert.pem \ - && mv cacert.pem /etc/ssl/certs/ca-certificates.crt + zip \ + opcache && \ + docker-php-ext-enable apcu COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer -FROM php-system-setup AS webserver-setup - -RUN apk add --update --no-cache \ - nginx \ - supervisor - COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf COPY ./docker/main/supervisord.conf /etc/supervisord.conf +COPY ./docker/main/www.conf /usr/local/etc/php-fpm.d/www.conf +COPY ./docker/main/local.ini /usr/local/etc/php/conf.d/local.ini -FROM webserver-setup AS app-setup +# FPM configuration via ENV +ENV FPM_PM=dynamic +ENV FPM_PM_MAX_CHILDREN=5 +ENV FPM_PM_START_SERVERS=2 +ENV FPM_PM_MIN_SPARE_SERVERS=1 +ENV FPM_PM_MAX_SPARE_SERVERS=3 + +# PHP configuration via ENV +ENV LOG_TO_STDOUT=1 +ENV PHP_MEMORY_LIMIT=512M +ENV PHP_MAX_EXECUTION_TIME=30 +ENV PHP_UPLOAD_MAX_FILESIZE=120M +ENV PHP_POST_MAX_SIZE=128M +ENV PHP_SESSION_SAVE_HANDLER=files +ENV PHP_SESSION_SAVE_PATH= +ENV PHP_DISPLAY_ERRORS=0 +ENV PHP_DISPLAY_STARTUP_ERRORS=0 + +FROM system-setup AS app-setup COPY . /app RUN chown -R nginx:nginx /app diff --git a/docker/main/default.conf b/docker/main/default.conf index a1814f4..8f99d47 100644 --- a/docker/main/default.conf +++ b/docker/main/default.conf @@ -1,10 +1,10 @@ server { - listen 80; - index index.php; server_name localhost; root /app/public; + index index.php; + error_log /dev/stdout; access_log /dev/stdout; diff --git a/docker/main/local.ini b/docker/main/local.ini new file mode 100644 index 0000000..00adf04 --- /dev/null +++ b/docker/main/local.ini @@ -0,0 +1,9 @@ +memory_limit = ${PHP_MEMORY_LIMIT} +max_execution_time = ${PHP_MAX_EXECUTION_TIME} +upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE} +post_max_size = ${PHP_POST_MAX_SIZE} +allow_url_fopen = 1 +session.save_handler = ${PHP_SESSION_SAVE_HANDLER} +session.save_path = ${PHP_SESSION_SAVE_PATH} +display_errors = ${PHP_DISPLAY_ERRORS} +display_startup_errors = ${PHP_DISPLAY_STARTUP_ERRORS} diff --git a/docker/main/www.conf b/docker/main/www.conf new file mode 100644 index 0000000..0d45d03 --- /dev/null +++ b/docker/main/www.conf @@ -0,0 +1,18 @@ +[global] +daemonize = no + +[www] +user = nginx +group = nginx +listen = 9000 +listen.owner = nginx +listen.group = nginx +listen.mode = 0660 + +pm = ${FPM_PM} +pm.max_children = ${FPM_PM_MAX_CHILDREN} +pm.start_servers = ${FPM_PM_START_SERVERS} +pm.min_spare_servers = ${FPM_PM_MIN_SPARE_SERVERS} +pm.max_spare_servers = ${FPM_PM_MAX_SPARE_SERVERS} + +catch_workers_output = yes From 3dd9d76f2f9aa14705b548087741e0407717de9e Mon Sep 17 00:00:00 2001 From: djoven89 Date: Wed, 25 Jun 2025 10:52:01 +0200 Subject: [PATCH 105/203] Add cache warm command to the entrypoint --- docker/main/entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh index 9d81720..5421176 100644 --- a/docker/main/entrypoint.sh +++ b/docker/main/entrypoint.sh @@ -14,6 +14,8 @@ if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then php bin/console doctrine:schema:update --force fi +php bin/console cache:warmup + echo "Start Supervisord" supervisord -c /etc/supervisord.conf From 5982517737c60ffcc485c333065740073e69f98f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Jul 2025 17:57:06 +0200 Subject: [PATCH 106/203] Do not ignore public bundles from docker context --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index b0ea767..a811162 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ -public/bundles/ var/ vendor/ From 25e29110717bd833bcc815d5ba8b273946f839d1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Jul 2025 16:45:41 +0200 Subject: [PATCH 107/203] Allow composer to run auto scripts on docker build --- docker/main/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 0cf0932..62181b0 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -60,7 +60,7 @@ RUN chmod +x /usr/local/bin/entrypoint.sh USER nginx WORKDIR /app -RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader +RUN composer install --prefer-dist --no-dev --optimize-autoloader EXPOSE 80 From 433fda5797adcf0ad7aeee4ac111e27803cbbbcd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Jul 2025 17:09:54 +0200 Subject: [PATCH 108/203] No scripts on dockerfile --- docker/main/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 62181b0..0cf0932 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -60,7 +60,7 @@ RUN chmod +x /usr/local/bin/entrypoint.sh USER nginx WORKDIR /app -RUN composer install --prefer-dist --no-dev --optimize-autoloader +RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader EXPOSE 80 From 02842b62a81658deafbae16e92de969e6aa1418c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Jul 2025 17:11:26 +0200 Subject: [PATCH 109/203] Install assets on app entrypoint --- docker/main/entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh index 5421176..c408ab1 100644 --- a/docker/main/entrypoint.sh +++ b/docker/main/entrypoint.sh @@ -15,6 +15,7 @@ if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then fi php bin/console cache:warmup +php bin/console assets:install echo "Start Supervisord" From 83b2a72ef3bd0ea46534451d0db06c8dc05db88d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 22 Jul 2025 13:51:49 +0200 Subject: [PATCH 110/203] Fall back to media property when video is null --- src/Benzina/ProjectsPump.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 4d29e78..1ee98e4 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -159,13 +159,22 @@ class ProjectsPump implements PumpInterface return $this->territoryService->search($cleanAddress); } - private function getProjectVideo(array $record): ?ProjectVideo + private function getProjectVideoSource(array $record): ?string { - if ($record['video'] === null) { - return null; + if ($record['video'] !== null) { + return \trim($record['video']); } - $url = \trim($record['video']); + if ($record['media'] !== null) { + return \trim($record['media']); + } + + return null; + } + + private function getProjectVideo(array $record): ?ProjectVideo + { + $url = $this->getProjectVideoSource($record); if ($url === '') { return null; From 8d383c931e59d6a9bc928f0ab39e04e145a7c1a6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 22 Jul 2025 13:52:15 +0200 Subject: [PATCH 111/203] Improve embed data retrieval management code --- src/Service/Embed/EmbedService.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Service/Embed/EmbedService.php b/src/Service/Embed/EmbedService.php index 49eab8f..bbd11bd 100644 --- a/src/Service/Embed/EmbedService.php +++ b/src/Service/Embed/EmbedService.php @@ -103,7 +103,11 @@ class EmbedService throw new \Exception(\sprintf('Could not extract embed data from %s', $url)); } - $data = $data[$url]; + $data = $data[\array_key_first($data)]; + + if (!\array_key_exists('html', $data)) { + throw new \Exception(\sprintf('Could not extract embed data from %s', $url)); + } return new EmbedVideo( $this->getIframeSrc($data['html']), From 821746ed143097d0034eea1b9629de213a9816aa Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Jul 2025 12:33:53 +0200 Subject: [PATCH 112/203] Add sorting on money and units, and filtering of hasUnits --- src/ApiResource/Project/RewardApiResource.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index e527ae0..9110c4b 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -2,6 +2,8 @@ namespace App\ApiResource\Project; +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; @@ -53,6 +55,7 @@ class RewardApiResource * The minimal monetary sum to be able to claim this reward. */ #[Assert\NotBlank()] + #[API\ApiFilter(OrderFilter::class, properties: ['money.amount'])] public Money $money; /** @@ -60,6 +63,7 @@ class RewardApiResource */ #[Assert\NotNull()] #[Assert\Type('bool')] + #[API\ApiFilter(BooleanFilter::class)] public bool $hasUnits; /** @@ -76,5 +80,6 @@ class RewardApiResource * For finite rewards, the currently available amount of unitsTotal that can be claimed. */ #[API\ApiProperty(writable: false)] + #[API\ApiFilter(OrderFilter::class)] public int $unitsAvailable = 0; } From be2a43c39d654d7ac0bd3d1794ae3699ee11d054 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 31 Jul 2025 14:09:13 +0200 Subject: [PATCH 113/203] Add indexes to transaction table --- src/Entity/Accounting/Transaction.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Entity/Accounting/Transaction.php b/src/Entity/Accounting/Transaction.php index 15d70d9..2de26fc 100644 --- a/src/Entity/Accounting/Transaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'accounting_transaction')] #[ORM\Entity(repositoryClass: TransactionRepository::class)] +#[ORM\Index(fields: ['origin'])] +#[ORM\Index(fields: ['target'])] class Transaction { use TimestampedCreationEntity; From 857706e982c833b01d08cc481384bc26d4b0a00d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 31 Jul 2025 14:14:04 +0200 Subject: [PATCH 114/203] Small optimizations on accounting trxs aggregated code --- .../Accounting/TransactionRepository.php | 14 +++++--------- src/Service/AccountingService.php | 10 ++++++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Repository/Accounting/TransactionRepository.php b/src/Repository/Accounting/TransactionRepository.php index a160df0..b44aaaf 100644 --- a/src/Repository/Accounting/TransactionRepository.php +++ b/src/Repository/Accounting/TransactionRepository.php @@ -34,19 +34,15 @@ class TransactionRepository extends ServiceEntityRepository ?\DateTimeInterface $dateEnd = null, ): array { $queryBuilder = $this->createQueryBuilder('t'); - $accountingExpr = $queryBuilder->expr(); - - if ($dateEnd === null) { - $dateEnd = new \DateTime(); - } $query = $queryBuilder - ->andWhere($accountingExpr->orX( - $accountingExpr->eq('t.origin', $accounting->getId()), - $accountingExpr->eq('t.target', $accounting->getId()), + ->andWhere($queryBuilder->expr()->orX( + 't.origin = :accountingId', + 't.target = :accountingId' )) + ->setParameter('accountingId', $accounting->getId()) ->andWhere('t.dateCreated <= :dateEnd') - ->setParameter('dateEnd', $dateEnd); + ->setParameter('dateEnd', $dateEnd ?? new \DateTime()); if ($dateStart !== null) { $query->andWhere('t.dateCreated >= :dateStart') diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index 6be9760..9c00bcd 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -30,12 +30,13 @@ class AccountingService $balance = new Money(0, $accounting->getCurrency()); $trxs = $this->transactionRepository->findByAccounting($accounting); + $accountingId = $accounting->getId(); foreach ($trxs as $transaction) { - if ($transaction->getTarget() === $accounting) { + if ($transaction->getTarget()->getId() === $accountingId) { $balance = $this->money->add($transaction->getMoney(), $balance); } - if ($transaction->getOrigin() === $accounting) { + if ($transaction->getOrigin()->getId() === $accountingId) { $balance = $this->money->substract($transaction->getMoney(), $balance); } } @@ -95,6 +96,7 @@ class AccountingService $points = []; $totalBalance = new Money(0, $accounting->getCurrency()); + $accountingId = $accounting->getId(); foreach ($period as $start) { $end = \DateTime::createFromInterface($start)->add($period->getDateInterval()); $periodTrxs = $this->getTransactionsInPeriod($trxs, $start, $end); @@ -103,11 +105,11 @@ class AccountingService foreach ($periodTrxs as $trx) { $trxMoney = $trx->getMoney(); - if ($trx->getTarget() === $accounting) { + if ($trx->getTarget()->getId() === $accountingId) { $balance = $this->money->add($trxMoney, $balance); } - if ($trx->getOrigin() === $accounting) { + if ($trx->getOrigin()->getId() === $accountingId) { $balance = $this->money->substract($trxMoney, $balance); } } From a892c449d17a886d1d4d319a50c59921465f4666 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 4 Aug 2025 12:42:17 +0200 Subject: [PATCH 115/203] Split simple Accounting and BalancedAccounting api resources --- .../Accounting/AccountingApiResource.php | 12 +----------- .../BalancedAccountingApiResource.php | 19 +++++++++++++++++++ .../Accounting/AccountingStateProvider.php | 3 ++- 3 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 src/ApiResource/Accounting/BalancedAccountingApiResource.php diff --git a/src/ApiResource/Accounting/AccountingApiResource.php b/src/ApiResource/Accounting/AccountingApiResource.php index bdcb8a9..91bd86a 100644 --- a/src/ApiResource/Accounting/AccountingApiResource.php +++ b/src/ApiResource/Accounting/AccountingApiResource.php @@ -10,14 +10,11 @@ use App\ApiResource\TipjarApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\Accounting\Accounting; use App\Entity\Matchfunding\MatchCall; -use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Tipjar; use App\Entity\User\User; -use App\Mapping\Transformer\AccountingBalanceMapTransformer; use App\State\Accounting\AccountingStateProcessor; use App\State\Accounting\AccountingStateProvider; -use AutoMapper\Attribute\MapFrom; /** * v4 features an advanced economy model under the hood. @@ -34,7 +31,7 @@ use AutoMapper\Attribute\MapFrom; processor: AccountingStateProcessor::class, )] #[API\GetCollection()] -#[API\Get()] +#[API\Get(output: BalancedAccountingApiResource::class)] #[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')] class AccountingApiResource { @@ -46,13 +43,6 @@ class AccountingApiResource */ public string $currency; - /** - * The money currently held by the Accounting. - */ - #[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)] - #[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')] - public Money $balance; - #[API\ApiProperty(readable: false, writable: false)] public string $ownerClass; diff --git a/src/ApiResource/Accounting/BalancedAccountingApiResource.php b/src/ApiResource/Accounting/BalancedAccountingApiResource.php new file mode 100644 index 0000000..8af3f15 --- /dev/null +++ b/src/ApiResource/Accounting/BalancedAccountingApiResource.php @@ -0,0 +1,19 @@ +autoMapper->map($accounting, AccountingApiResource::class); + return $this->autoMapper->map($accounting, BalancedAccountingApiResource::class); } } From e3dc2ffa7571a702d40ebab21bbec1426a591687 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 4 Aug 2025 15:55:21 +0200 Subject: [PATCH 116/203] Update accounting balance calc with simplified trx data --- .../Accounting/TransactionRepository.php | 39 +++++++++++++++++++ src/Service/AccountingService.php | 16 ++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/Repository/Accounting/TransactionRepository.php b/src/Repository/Accounting/TransactionRepository.php index b44aaaf..d2c70f9 100644 --- a/src/Repository/Accounting/TransactionRepository.php +++ b/src/Repository/Accounting/TransactionRepository.php @@ -54,6 +54,45 @@ class TransactionRepository extends ServiceEntityRepository return $query->getQuery()->getResult(); } + /** + * @param \DateTimeInterface|null $dateStart a date to act as lower bound of the selection + * @param \DateTimeInterface|null $dateEnd A date to act as upper bound of the selection. Current date will be used if null. + * + * @return array Simplified array data of Transactions originated from or targeting the Accounting + */ + public function findForBalanceCalc( + Accounting $accounting, + ?\DateTimeInterface $dateStart = null, + ?\DateTimeInterface $dateEnd = null, + ): array { + $queryBuilder = $this->createQueryBuilder('t'); + + $query = $queryBuilder + ->select(' + t.id, + t.money.amount, + t.money.currency, + IDENTITY(t.origin) AS origin_id, + IDENTITY(t.target) AS target_id + ') + ->andWhere($queryBuilder->expr()->orX( + 't.origin = :accountingId', + 't.target = :accountingId' + )) + ->setParameter('accountingId', $accounting->getId()) + ->andWhere('t.dateCreated <= :dateEnd') + ->setParameter('dateEnd', $dateEnd ?? new \DateTime()); + + if ($dateStart !== null) { + $query->andWhere('t.dateCreated >= :dateStart') + ->setParameter('dateStart', $dateStart); + } + + $query->orderBy('t.id', 'ASC'); + + return $query->getQuery()->getArrayResult(); + } + /** * @return Transaction[] Transactions originated from the Accounting */ diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index 9c00bcd..66ab530 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -27,17 +27,19 @@ class AccountingService return $this->wallet->getBalance($accounting); } - $balance = new Money(0, $accounting->getCurrency()); - $trxs = $this->transactionRepository->findByAccounting($accounting); + $trxs = $this->transactionRepository->findForBalanceCalc($accounting); $accountingId = $accounting->getId(); - foreach ($trxs as $transaction) { - if ($transaction->getTarget()->getId() === $accountingId) { - $balance = $this->money->add($transaction->getMoney(), $balance); + $balance = new Money(0, $accounting->getCurrency()); + foreach ($trxs as $trx) { + $transaction = new Money($trx['money.amount'], $trx['money.currency']); + + if ($trx['target_id'] === $accountingId) { + $balance = $this->money->add($transaction, $balance); } - if ($transaction->getOrigin()->getId() === $accountingId) { - $balance = $this->money->substract($transaction->getMoney(), $balance); + if ($trx['origin_id'] === $accountingId) { + $balance = $this->money->substract($transaction, $balance); } } From 6bd87eae1c5fd05af3628be553ca1cde150a2a38 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 4 Aug 2025 15:56:21 +0200 Subject: [PATCH 117/203] Split balanced accountings for single account retrieval --- .../Accounting/AccountingApiResource.php | 24 ++++---- .../BalancedAccountingApiResource.php | 60 ++++++++++++++++++- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/ApiResource/Accounting/AccountingApiResource.php b/src/ApiResource/Accounting/AccountingApiResource.php index 91bd86a..85c3f16 100644 --- a/src/ApiResource/Accounting/AccountingApiResource.php +++ b/src/ApiResource/Accounting/AccountingApiResource.php @@ -46,6 +46,18 @@ class AccountingApiResource #[API\ApiProperty(readable: false, writable: false)] public string $ownerClass; + #[API\ApiProperty(readable: false, writable: false)] + public ?UserApiResource $user = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?ProjectApiResource $project = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?TipjarApiResource $tipjar = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?MatchCallApiResource $matchCall; + /** * The resource owning this Accounting. * @@ -66,16 +78,4 @@ class AccountingApiResource return null; } - - #[API\ApiProperty(readable: false, writable: false)] - public ?UserApiResource $user = null; - - #[API\ApiProperty(readable: false, writable: false)] - public ?ProjectApiResource $project = null; - - #[API\ApiProperty(readable: false, writable: false)] - public ?TipjarApiResource $tipjar = null; - - #[API\ApiProperty(readable: false, writable: false)] - public ?MatchCallApiResource $matchCall; } diff --git a/src/ApiResource/Accounting/BalancedAccountingApiResource.php b/src/ApiResource/Accounting/BalancedAccountingApiResource.php index 8af3f15..6d0a975 100644 --- a/src/ApiResource/Accounting/BalancedAccountingApiResource.php +++ b/src/ApiResource/Accounting/BalancedAccountingApiResource.php @@ -3,17 +3,75 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Metadata as API; +use App\ApiResource\Matchfunding\MatchCallApiResource; +use App\ApiResource\Project\ProjectApiResource; +use App\ApiResource\TipjarApiResource; +use App\ApiResource\User\UserApiResource; use App\Entity\Accounting\Accounting; +use App\Entity\Matchfunding\MatchCall; use App\Entity\Money; +use App\Entity\Project\Project; +use App\Entity\Tipjar; +use App\Entity\User\User; use App\Mapping\Transformer\AccountingBalanceMapTransformer; use AutoMapper\Attribute\MapFrom; +use Symfony\Component\Serializer\Attribute\Ignore; -class BalancedAccountingApiResource extends AccountingApiResource +class BalancedAccountingApiResource { + public int $id; + + /** + * The preferred currency for monetary operations.\ + * 3-letter ISO 4217 currency code. + */ + public string $currency; + /** * The money currently held by the Accounting. */ #[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)] #[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')] public Money $balance; + + #[Ignore] + #[API\ApiProperty(readable: false, writable: false)] + public string $ownerClass; + + #[Ignore] + #[API\ApiProperty(readable: false, writable: false)] + public ?UserApiResource $user = null; + + #[Ignore] + #[API\ApiProperty(readable: false, writable: false)] + public ?ProjectApiResource $project = null; + + #[Ignore] + #[API\ApiProperty(readable: false, writable: false)] + public ?TipjarApiResource $tipjar = null; + + #[Ignore] + #[API\ApiProperty(readable: false, writable: false)] + public ?MatchCallApiResource $matchCall; + + /** + * The resource owning this Accounting. + * + * @return UserApiResource|ProjectApiResource|TipjarApiResource + */ + public function getOwner(): ?object + { + switch ($this->ownerClass) { + case User::class: + return $this->user; + case Project::class: + return $this->project; + case Tipjar::class: + return $this->tipjar; + case MatchCall::class: + return $this->matchCall; + } + + return null; + } } From 9c425e38ef496c39d7f169ad149ca5df685f76f3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 4 Aug 2025 15:56:34 +0200 Subject: [PATCH 118/203] Enable CECA Gateway --- src/Gateway/Gateway/CecaGateway.php | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Gateway/Gateway/CecaGateway.php b/src/Gateway/Gateway/CecaGateway.php index 6f0771e..63d7d00 100644 --- a/src/Gateway/Gateway/CecaGateway.php +++ b/src/Gateway/Gateway/CecaGateway.php @@ -2,12 +2,47 @@ namespace App\Gateway\Gateway; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\Checkout; +use App\Gateway\ChargeType; use App\Gateway\GatewayInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -abstract class CecaGateway implements GatewayInterface +class CecaGateway implements GatewayInterface { + public const LEGACY_MESSAGE = 'This Gateway implementation is only meant to give support to legacy-records. DO NOT USE. DO NOT IMPLEMENT.'; + public static function getName(): string { return 'tpv'; } + + public static function getSupportedChargeTypes(): array + { + return [ + ChargeType::Single, + ]; + } + + public function process(Checkout $checkout): Checkout + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function handleRedirect(Request $request): RedirectResponse + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function handleWebhook(Request $request): Response + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function processRefund(Charge $charge): void + { + throw new \Exception(self::LEGACY_MESSAGE); + } } From 6f19acaa83c13205922bae341b4090f4640c9a6f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 5 Aug 2025 18:22:11 +0200 Subject: [PATCH 119/203] Remove balance from regular accounting api resource --- src/ApiResource/Accounting/AccountingApiResource.php | 2 +- src/State/Accounting/AccountingStateProvider.php | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ApiResource/Accounting/AccountingApiResource.php b/src/ApiResource/Accounting/AccountingApiResource.php index 85c3f16..74a4ee6 100644 --- a/src/ApiResource/Accounting/AccountingApiResource.php +++ b/src/ApiResource/Accounting/AccountingApiResource.php @@ -31,7 +31,7 @@ use App\State\Accounting\AccountingStateProvider; processor: AccountingStateProcessor::class, )] #[API\GetCollection()] -#[API\Get(output: BalancedAccountingApiResource::class)] +#[API\Get()] #[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')] class AccountingApiResource { diff --git a/src/State/Accounting/AccountingStateProvider.php b/src/State/Accounting/AccountingStateProvider.php index a993d48..d8260d0 100644 --- a/src/State/Accounting/AccountingStateProvider.php +++ b/src/State/Accounting/AccountingStateProvider.php @@ -9,9 +9,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\Pagination\TraversablePaginator; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Accounting\AccountingApiResource; -use App\ApiResource\Accounting\BalancedAccountingApiResource; use App\Mapping\AutoMapper; -use App\Service\AccountingService; use Symfony\Component\DependencyInjection\Attribute\Autowire; class AccountingStateProvider implements ProviderInterface @@ -22,7 +20,6 @@ class AccountingStateProvider implements ProviderInterface #[Autowire(service: CollectionProvider::class)] private ProviderInterface $collectionProvider, private AutoMapper $autoMapper, - private AccountingService $accountingService, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null @@ -49,6 +46,6 @@ class AccountingStateProvider implements ProviderInterface return null; } - return $this->autoMapper->map($accounting, BalancedAccountingApiResource::class); + return $this->autoMapper->map($accounting, AccountingApiResource::class); } } From 092656d0dcd50ca7cb865055ead5dabad4ab06ce Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 5 Aug 2025 18:22:39 +0200 Subject: [PATCH 120/203] Provide balances via dedicated AccountingBalance resource --- .../Accounting/AccountingBalance.php | 39 ++++++++++ .../BalancedAccountingApiResource.php | 77 ------------------- .../AccountingBalanceMapTransformer.php | 22 ------ src/OpenApi/RedocExtensionsTrait.php | 1 + .../AccountingBalanceStateProvider.php | 42 ++++++++++ 5 files changed, 82 insertions(+), 99 deletions(-) create mode 100644 src/ApiResource/Accounting/AccountingBalance.php delete mode 100644 src/ApiResource/Accounting/BalancedAccountingApiResource.php delete mode 100644 src/Mapping/Transformer/AccountingBalanceMapTransformer.php create mode 100644 src/State/Accounting/AccountingBalanceStateProvider.php diff --git a/src/ApiResource/Accounting/AccountingBalance.php b/src/ApiResource/Accounting/AccountingBalance.php new file mode 100644 index 0000000..4bb9c71 --- /dev/null +++ b/src/ApiResource/Accounting/AccountingBalance.php @@ -0,0 +1,39 @@ + new API\Link( + fromClass: AccountingApiResource::class, + fromProperty: 'id', + description: 'Accounting identifier' + ), + ] +)] +class AccountingBalance +{ + #[API\ApiProperty(identifier: true)] + public AccountingApiResource $accounting; + + /** + * The money currently held by the Accounting. + */ + #[API\ApiProperty(security: 'is_granted("ACCOUNTING_VIEW", object.accounting)')] + public Money $balance; +} diff --git a/src/ApiResource/Accounting/BalancedAccountingApiResource.php b/src/ApiResource/Accounting/BalancedAccountingApiResource.php deleted file mode 100644 index 6d0a975..0000000 --- a/src/ApiResource/Accounting/BalancedAccountingApiResource.php +++ /dev/null @@ -1,77 +0,0 @@ -ownerClass) { - case User::class: - return $this->user; - case Project::class: - return $this->project; - case Tipjar::class: - return $this->tipjar; - case MatchCall::class: - return $this->matchCall; - } - - return null; - } -} diff --git a/src/Mapping/Transformer/AccountingBalanceMapTransformer.php b/src/Mapping/Transformer/AccountingBalanceMapTransformer.php deleted file mode 100644 index c829b35..0000000 --- a/src/Mapping/Transformer/AccountingBalanceMapTransformer.php +++ /dev/null @@ -1,22 +0,0 @@ -accountingService->calcBalance($source); - } -} diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index a9fa08d..a76323c 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -61,6 +61,7 @@ trait RedocExtensionsTrait 'name' => 'Accounting', 'tags' => [ 'Accounting', + 'AccountingBalance', 'AccountingBalancePoint', 'AccountingTransaction', ], diff --git a/src/State/Accounting/AccountingBalanceStateProvider.php b/src/State/Accounting/AccountingBalanceStateProvider.php new file mode 100644 index 0000000..b34cd72 --- /dev/null +++ b/src/State/Accounting/AccountingBalanceStateProvider.php @@ -0,0 +1,42 @@ +accountingRepository->find($uriVariables['id']); + + if ($accounting === null) { + return null; + } + + $balance = new AccountingBalance(); + + /** @var AccountingApiResource */ + $resource = $this->autoMapper->map($accounting, AccountingApiResource::class); + + $balance->accounting = $resource; + $balance->balance = $this->accountingService->calcBalance($accounting); + + return $balance; + } +} From 92e7e0dd2e56858e1494c7e7688177a1665f3a49 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 25 Aug 2025 15:43:44 +0200 Subject: [PATCH 121/203] Add ToCampaign ProjectStatus case --- src/Entity/Project/ProjectStatus.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Entity/Project/ProjectStatus.php b/src/Entity/Project/ProjectStatus.php index 388fa95..ba151fe 100644 --- a/src/Entity/Project/ProjectStatus.php +++ b/src/Entity/Project/ProjectStatus.php @@ -32,6 +32,11 @@ enum ProjectStatus: string */ case Rejected = 'rejected'; + /** + * An admin reviewed it and deemed worthy to campaign. + */ + case ToCampaign = 'to_campaign'; + /** * Project was reviewed and is in campaign for funding. */ From 04ba254d1955592125a2743495c485fb84fb92f1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 12:08:43 +0200 Subject: [PATCH 122/203] Refactor ProjectSupport into origin and Transactions --- .../Project/SupportApiResource.php | 42 +++++++++------- src/Entity/Project/Support.php | 48 +++++++++---------- .../GatewayCheckoutSupportsListener.php | 32 +++++++------ .../SupportMoneyMapTransformer.php | 6 +-- 4 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index f448205..c5286f4 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -5,8 +5,8 @@ namespace App\ApiResource\Project; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; -use App\ApiResource\Gateway\ChargeApiResource; -use App\ApiResource\User\UserApiResource; +use App\ApiResource\Accounting\AccountingApiResource; +use App\ApiResource\Accounting\TransactionApiResource; use App\Entity\Money; use App\Entity\Project\Support; use App\Mapping\Transformer\SupportMoneyMapTransformer; @@ -15,6 +15,12 @@ use App\State\Project\SupportStateProcessor; use AutoMapper\Attribute\MapFrom; use Symfony\Component\Validator\Constraints as Assert; +/** + * ProjectSupports gather Transactions going from one same origin to one same Project.\ + * \ + * MatchCalls specially might make several different Transactions to the same Project, + * but their ProjectSupport remains the same just with updated money. + */ #[API\ApiResource( shortName: 'ProjectSupport', stateOptions: new Options(entityClass: Support::class), @@ -32,31 +38,31 @@ class SupportApiResource public int $id; /** - * The User who created the ProjectSupport record.\ - * \ - * When `anonymous` is *true* it will only be public to admins and the User. - */ - #[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')] - #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] - public ?UserApiResource $owner; - - /** - * The Project being targeted in the Charges. + * The Project being supported. */ #[API\ApiProperty(writable: false)] #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] public ProjectApiResource $project; /** - * The Charges that were paid by the User. - * - * @var array + * The Accounting of origin for the Transactions under this ProjectSupport record.\ + * \ + * When `anonymous` is *true* it will only be public to admins and the User. */ - #[API\ApiProperty(writable: false)] - public array $charges; + #[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')] + #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] + public ?AccountingApiResource $origin; /** - * The total monetary value of the Charges paid by the User. + * The Transactions that were issued to the Project by the origin. + * + * @var array + */ + #[API\ApiProperty(writable: false)] + public array $transactions; + + /** + * The total monetary value of the Transactions going to the Project. */ #[MapFrom(Support::class, transformer: SupportMoneyMapTransformer::class)] public Money $money; diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index efa8d18..9d334dc 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -2,8 +2,8 @@ namespace App\Entity\Project; -use App\Entity\Gateway\Charge; -use App\Entity\User\User; +use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\SupportRepository; use AutoMapper\Attribute\MapProvider; @@ -21,22 +21,22 @@ class Support #[ORM\Column] private ?int $id = null; - #[ORM\ManyToOne(inversedBy: 'supports')] - #[ORM\JoinColumn(nullable: false)] - private ?User $owner = null; - #[ORM\ManyToOne(inversedBy: 'supports')] #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: false)] + private ?Accounting $origin = null; + /** - * @var Collection + * @var Collection */ - #[ORM\JoinTable(name: 'project_support_charges')] + #[ORM\JoinTable(name: 'project_support_trxs')] #[ORM\JoinColumn(name: 'support_id', referencedColumnName: 'id')] - #[ORM\InverseJoinColumn(name: 'charge_id', referencedColumnName: 'id')] - #[ORM\ManyToMany(targetEntity: Charge::class)] - private Collection $charges; + #[ORM\InverseJoinColumn(name: 'transaction_id', referencedColumnName: 'id')] + #[ORM\ManyToMany(targetEntity: Transaction::class)] + private Collection $transactions; #[ORM\Column] private ?bool $anonymous = null; @@ -46,7 +46,7 @@ class Support public function __construct() { - $this->charges = new ArrayCollection(); + $this->transactions = new ArrayCollection(); } public function getId(): ?int @@ -54,14 +54,14 @@ class Support return $this->id; } - public function getOwner(): ?User + public function getOrigin(): ?Accounting { - return $this->owner; + return $this->origin; } - public function setOwner(?User $owner): static + public function setOrigin(?Accounting $origin): static { - $this->owner = $owner; + $this->origin = $origin; return $this; } @@ -79,25 +79,25 @@ class Support } /** - * @return Collection + * @return Collection */ - public function getCharges(): Collection + public function getTransactions(): Collection { - return $this->charges; + return $this->transactions; } - public function addCharge(Charge $charge): static + public function addTransaction(Transaction $transaction): static { - if (!$this->charges->contains($charge)) { - $this->charges->add($charge); + if (!$this->transactions->contains($transaction)) { + $this->transactions->add($transaction); } return $this; } - public function removeCharge(Charge $charge): static + public function removeTransaction(Transaction $transaction): static { - $this->charges->removeElement($charge); + $this->transactions->removeElement($transaction); return $this; } diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index f47e16e..337e98d 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -2,11 +2,11 @@ namespace App\EventListener; +use App\Entity\Accounting\Accounting; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Project\Project; use App\Entity\Project\Support; -use App\Entity\User\User; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; @@ -55,18 +55,16 @@ class GatewayCheckoutSupportsListener /** * Create ProjectSupport for the given data. - * - * @param Charge[] $charges */ - private function createSupport(Project $project, User $owner, array $charges): Support + private function createSupport(Project $project, Accounting $origin, array $transactions): Support { $projectSupport = new Support(); $projectSupport->setProject($project); - $projectSupport->setOwner($owner); + $projectSupport->setOrigin($origin); $projectSupport->setAnonymous(false); - foreach ($charges as $charge) { - $projectSupport->addCharge($charge); + foreach ($transactions as $transaction) { + $projectSupport->addTransaction($transaction); } return $projectSupport; @@ -77,24 +75,28 @@ class GatewayCheckoutSupportsListener */ public function prepareSupports(Checkout $checkout): array { + /** @var Charge[] */ $charges = $checkout->getCharges()->toArray(); - $owner = $checkout->getOrigin()->getUser(); + $origin = $checkout->getOrigin(); - // Group charges for projects - $chargesInProjectMap = []; + $projects = []; + $transactionsByProject = []; foreach ($charges as $charge) { $project = $charge->getTarget()->getProject(); - if ($project === null) { + + if (!$project) { continue; } - $chargesInProjectMap[$project->getId()][] = $charge; + $projectId = $project->getId(); + + $projects[$projectId] = $project; + $transactionsByProject[$projectId] = [...$transactionsByProject[$projectId], ...$charge->getTransactions()]; } $supports = []; - foreach ($chargesInProjectMap as $chargesInProject) { - $project = $chargesInProject[0]->getTarget()->getProject(); - $supports[] = $this->createSupport($project, $owner, $chargesInProject); + foreach ($transactionsByProject as $projectId => $transactions) { + $supports[] = $this->createSupport($projects[$projectId], $origin, $transactions); } return $supports; diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php index a123a25..b5998aa 100644 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ b/src/Mapping/Transformer/SupportMoneyMapTransformer.php @@ -20,9 +20,9 @@ class SupportMoneyMapTransformer implements PropertyTransformerInterface { $money = new Money(0, $source->getProject()->getAccounting()->getCurrency()); - $charges = $source->getCharges(); - foreach ($charges as $charge) { - $money = $this->moneyService->add($charge->getMoney(), $money); + $transactions = $source->getTransactions(); + foreach ($transactions as $transaction) { + $money = $this->moneyService->add($transaction->getMoney(), $money); } return $money; From f45a77290044d680cffe778b7ca923d1f4f9a2ac Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 12:20:16 +0200 Subject: [PATCH 123/203] Simplify calendar setting on projects pumping --- src/Benzina/ProjectsPump.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 1ee98e4..65e27ee 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -3,6 +3,7 @@ namespace App\Benzina; use App\Entity\Project\Project; +use App\Entity\Project\ProjectCalendar; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; @@ -12,7 +13,6 @@ use App\Entity\Territory; use App\Entity\User\User; use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; -use App\Service\Project\CalendarService; use App\Service\Project\TerritoryService; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; @@ -29,7 +29,6 @@ class ProjectsPump implements PumpInterface private UserRepository $userRepository, private TerritoryService $territoryService, private EmbedService $embedService, - private CalendarService $calendarService, ) {} public function supports(mixed $sample): bool @@ -81,12 +80,7 @@ class ProjectsPump implements PumpInterface $conf = $this->getProjectConf($project, $context); $project->setDeadline($this->getProjectDeadline($conf)); - $project->setCalendar($this->calendarService->makeCalendar( - $project->getDeadline(), - new \DateTimeImmutable($record['published']), - $conf['days_round1'], - $conf['days_round2'], - )); + $project->setCalendar($this->getProjectCalendar($record)); $this->persist($project, $context); } @@ -96,6 +90,16 @@ class ProjectsPump implements PumpInterface return $this->userRepository->findOneBy(['migratedId' => $record['owner']]); } + private function getProjectCalendar(array $record): ProjectCalendar + { + $calendar = new ProjectCalendar(); + $calendar->release = $record['published']; + $calendar->minimum = $record['passed']; + $calendar->optimum = $record['success']; + + return $calendar; + } + private function getProjectStatus(array $record): ProjectStatus { switch ($record['status']) { From ab6b1942026fa1bbbf409cffb7018643b50835bd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 13:51:13 +0200 Subject: [PATCH 124/203] Pump composed project description and localizations --- src/Benzina/ProjectsPump.php | 46 ++++++++++++++++++++++++++++--- src/Benzina/ProjectsPumpTrait.php | 43 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 65e27ee..929116d 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -57,13 +57,9 @@ class ProjectsPump implements PumpInterface } $project = new Project(); - $project->setTranslatableLocale($record['lang']); - $project->setTitle($record['name']); $project->setSlug($record['id']); - $project->setSubtitle($record['subtitle']); $project->setCategory($this->getProjectCategory($record)); $project->setTerritory($this->getProjectTerritory($record)); - $project->setDescription($record['description']); $project->setVideo($this->getProjectVideo($record)); $project->setOwner($owner); $project->setStatus($status); @@ -72,6 +68,19 @@ class ProjectsPump implements PumpInterface $project->setDateCreated(new \DateTime($record['created'])); $project->setDateUpdated(new \DateTime()); + $project->setTranslatableLocale($record['lang']); + $project->setTitle($record['name']); + $project->setSubtitle($record['subtitle']); + $project->setDescription($this->getProjectDescription($record)); + + $localizations = $this->getProjectLocalizations($project, $context); + foreach ($localizations as $localization) { + $project->setTranslatableLocale($localization['lang']); + $project->setTitle($localization['name']); + $project->setSubtitle($localization['subtitle']); + $project->setDescription($this->getProjectDescription($localization)); + } + $updates = $this->getProjectUpdates($project, $context); foreach ($updates as $update) { $project->addUpdate($update); @@ -90,6 +99,35 @@ class ProjectsPump implements PumpInterface return $this->userRepository->findOneBy(['migratedId' => $record['owner']]); } + private function getProjectDescription(array $record): string + { + $hasTitles = \array_key_exists($record['lang'], self::PROJECT_DESC_TITLES); + + $description = $record['description']; + + $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['about'] : ''); + $description .= \sprintf("\n%s", $record['about']); + + $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['motivation'] : ''); + $description .= \sprintf("\n%s", $record['motivation']); + + $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['related'] : ''); + $description .= \sprintf("\n%s", $record['related']); + + return $description; + } + + private function getProjectLocalizations(Project $project, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `project_lang` l WHERE l.id = :project' + ); + + $query->execute(['project' => $project->getMigratedId()]); + + return $query->fetchAll(); + } + private function getProjectCalendar(array $record): ProjectCalendar { $calendar = new ProjectCalendar(); diff --git a/src/Benzina/ProjectsPumpTrait.php b/src/Benzina/ProjectsPumpTrait.php index 1069c2e..9bf33a1 100644 --- a/src/Benzina/ProjectsPumpTrait.php +++ b/src/Benzina/ProjectsPumpTrait.php @@ -127,6 +127,49 @@ trait ProjectsPumpTrait 'PAMLONA' => ['IRUÑEA'], ]; + private const PROJECT_DESC_TITLES = [ + 'es' => [ + 'about' => 'Descripción del proyecto. Características, fortalezas y diferenciales.', + 'motivation' => 'Motivación y a quién va dirigido el proyecto.', + 'related' => 'Experiencia previa y equipo.', + ], + 'gl' => [ + 'about' => 'Características básicas.', + 'motivation' => 'Motivación e a quen vai dirixido o proxecto.', + 'related' => 'Experiencia previa e equipo.', + ], + 'eu' => [ + 'about' => 'Oinarrizko ezaugarriak eta helburuak.', + 'motivation' => 'Zerk bultzatzen zaitu eta zeini zuzenduta dago proiektua.', + 'related' => 'Proiektuaren deskribapenean derrigorrekoa da azaltzea kontutan hartzen den taldearen esperientzia.', + ], + 'ca' => [ + 'about' => 'Característiques bàsiques i objectius.', + 'motivation' => 'Motivació i a qui va dirigit el projecte.', + 'related' => 'Experiència prèvia i equip.', + ], + 'en' => [ + 'about' => 'Main features and goals of the crowdfunding campaign.', + 'motivation' => 'Why this is important.', + 'related' => 'Team and experience.', + ], + 'fr' => [ + 'about' => 'Caractéristiques principales.', + 'motivation' => 'Pourquoi est-ce important.', + 'related' => 'Expérience antérieure et équipe.', + ], + 'it' => [ + 'about' => 'Caratteristiche fondamentali.', + 'motivation' => 'Obiettivo e a chi si dirige il progetto.', + 'related' => 'Esperienza e team.', + ], + 'de' => [ + 'about' => 'Hauptmerkmale.', + 'motivation' => 'Warum das wichtig ist.', + 'related' => 'Team und Erfahrung.', + ], + ]; + private const PROJECT_KEYS = [ 'id', 'name', From c368eaa21bd1e1671a010de10434dbf5c736cdb2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 14:45:37 +0200 Subject: [PATCH 125/203] Fallback to record values on missing localization values --- src/Benzina/ProjectsPump.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 929116d..88082fa 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -76,8 +76,8 @@ class ProjectsPump implements PumpInterface $localizations = $this->getProjectLocalizations($project, $context); foreach ($localizations as $localization) { $project->setTranslatableLocale($localization['lang']); - $project->setTitle($localization['name']); - $project->setSubtitle($localization['subtitle']); + $project->setTitle($localization['name'] ?? $record['name']); + $project->setSubtitle($localization['subtitle'] ?? $record['subtitle']); $project->setDescription($this->getProjectDescription($localization)); } From e22f0d94c354d69ae5fd9620791600e19906d788 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 14:48:58 +0200 Subject: [PATCH 126/203] Pump record calendar values as DateTimes --- src/Benzina/ProjectsPump.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 88082fa..016e680 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -131,9 +131,9 @@ class ProjectsPump implements PumpInterface private function getProjectCalendar(array $record): ProjectCalendar { $calendar = new ProjectCalendar(); - $calendar->release = $record['published']; - $calendar->minimum = $record['passed']; - $calendar->optimum = $record['success']; + $calendar->release = new \DateTimeImmutable($record['published']); + $calendar->minimum = new \DateTimeImmutable($record['passed']); + $calendar->optimum = new \DateTimeImmutable($record['success']); return $calendar; } From bd702ba954b037834d2f4f33687319de2c3a040a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 15:58:09 +0200 Subject: [PATCH 127/203] Fix Project pumping of localizations --- src/Benzina/ProjectsPump.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 016e680..bc94766 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -14,6 +14,7 @@ use App\Entity\User\User; use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; use App\Service\Project\TerritoryService; +use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -74,11 +75,16 @@ class ProjectsPump implements PumpInterface $project->setDescription($this->getProjectDescription($record)); $localizations = $this->getProjectLocalizations($project, $context); + $translations = $this->entityManager->getRepository(Translation::class); foreach ($localizations as $localization) { - $project->setTranslatableLocale($localization['lang']); - $project->setTitle($localization['name'] ?? $record['name']); - $project->setSubtitle($localization['subtitle'] ?? $record['subtitle']); - $project->setDescription($this->getProjectDescription($localization)); + $locale = $localization['lang']; + + $project->addLocale($locale); + $translations + ->translate($project, 'title', $locale, $localization['name'] ?? $record['name']) + ->translate($project, 'subtitle', $locale, $localization['subtitle'] ?? $record['subtitle']) + ->translate($project, 'description', $locale, $this->getProjectDescription($localization)) + ; } $updates = $this->getProjectUpdates($project, $context); @@ -262,7 +268,7 @@ class ProjectsPump implements PumpInterface $update->setTranslatableLocale($project->getLocales()[0]); $update->setTitle($post['title']); $update->setSubtitle($post['subtitle'] ?? ''); - $update->setBody($post['text']); + $update->setBody($post['text'] ?? ''); $update->setDate(new \DateTime($post['date'])); $updates[] = $update; From f0d2dead2da49fc4c8375f24bcc5d197c3c99980 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 15:58:57 +0200 Subject: [PATCH 128/203] Fix single project retrieval without localizations --- src/State/Project/ProjectStateProvider.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/State/Project/ProjectStateProvider.php b/src/State/Project/ProjectStateProvider.php index 4aa8552..418548e 100644 --- a/src/State/Project/ProjectStateProvider.php +++ b/src/State/Project/ProjectStateProvider.php @@ -5,11 +5,14 @@ namespace App\State\Project; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Project\ProjectApiResource; +use App\Doctrine\LocalizedExtensionTrait; use App\Mapping\AutoMapper; use App\Repository\Project\ProjectRepository; class ProjectStateProvider implements ProviderInterface { + use LocalizedExtensionTrait; + public function __construct( private ProjectRepository $projectRepository, private AutoMapper $autoMapper, @@ -17,7 +20,15 @@ class ProjectStateProvider implements ProviderInterface public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $project = $this->projectRepository->findOneByIdOrSlug($uriVariables['idOrSlug']); + $idOrSlug = $uriVariables['idOrSlug']; + + $queryBuilder = $this->projectRepository->createQueryBuilder('p'); + $queryBuilder->where(\is_numeric($idOrSlug) ? 'p.id = :value' : 'p.slug = :value'); + $queryBuilder->setParameter('value', $idOrSlug); + + $query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context)); + + $project = $query->getOneOrNullResult(); if ($project === null) { return null; From d1ecc8fcc3014a6218ce784ffe41f0fdaa33e9c6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 16:30:48 +0200 Subject: [PATCH 129/203] Skip not published project updates --- src/Benzina/ProjectsPump.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index bc94766..9c76b7b 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -263,6 +263,10 @@ class ProjectsPump implements PumpInterface $posts = $this->getProjectBlogPosts($project, $context); foreach ($posts as $post) { + if ($post['publish'] == 0) { + continue; + } + $update = new Update(); $update->setProject($project); $update->setTranslatableLocale($project->getLocales()[0]); From 96674d3b2b92e70560191bd379ee9fe994865c5d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 17:02:20 +0200 Subject: [PATCH 130/203] Defend against ECB returning empty exchange rates --- .../Economy/Currency/EuropeanCentralBankExchange.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php b/src/Library/Economy/Currency/EuropeanCentralBankExchange.php index 7fc7508..976e247 100644 --- a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php +++ b/src/Library/Economy/Currency/EuropeanCentralBankExchange.php @@ -137,7 +137,13 @@ class EuropeanCentralBankExchange implements ExchangeInterface throw new \Exception('Could not retrieve XML data'); } - return \json_decode(\json_encode($data), true)['Cube']['Cube']; + $data = \json_decode(\json_encode($data), true)['Cube']['Cube']; + + if (empty($data['@attributes']['time'])) { + throw new \Exception('XML data comes empty from ECB'); + } + + return $data; } private function getDataCached(): array @@ -148,7 +154,7 @@ class EuropeanCentralBankExchange implements ExchangeInterface return $this->getDataLatest(); }); - if (!$data) { + if (!$data || empty($data['@attributes']['time'])) { throw new \Exception('Could not retrieve cached data'); } From f6372c0af2aae8a81190ea134411dfae3313138c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 17:22:55 +0200 Subject: [PATCH 131/203] Add FrankfurterExchange --- .../Economy/Currency/FrankfurterExchange.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/Library/Economy/Currency/FrankfurterExchange.php diff --git a/src/Library/Economy/Currency/FrankfurterExchange.php b/src/Library/Economy/Currency/FrankfurterExchange.php new file mode 100644 index 0000000..8aa7a54 --- /dev/null +++ b/src/Library/Economy/Currency/FrankfurterExchange.php @@ -0,0 +1,76 @@ +request('GET', self::ENDPOINT); + $data = $response->toArray(); + + $provider = new ConfigurableProvider(); + foreach ($data['rates'] as $currency => $rate) { + $provider->setExchangeRate(self::ISO_4217, $currency, $rate); + } + + $this->date = new \DateTimeImmutable($data['date']); + $this->provider = new BaseCurrencyProvider($provider, self::ISO_4217); + $this->converter = new CurrencyConverter($this->provider); + } + + public function convert(Money $money, string $toCurrency): Money + { + $converted = $this->converter->convert( + MoneyService::toBrick($money), + $toCurrency, + null, + RoundingMode::HALF_EVEN + ); + + return new Money( + $converted->getMinorAmount()->toInt(), + $converted->getCurrency()->getCurrencyCode() + ); + } + + public function getConversionRate(string $fromCurrency, string $toCurrency): float + { + return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); + } + + public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface + { + return $this->date; + } +} From 0520ea0d7d89f18bbd6f34092b7311cf701d77b2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 2 Sep 2025 17:39:57 +0200 Subject: [PATCH 132/203] Add trailing coma (php-cs-fixer) --- src/Library/Economy/Currency/FrankfurterExchange.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Library/Economy/Currency/FrankfurterExchange.php b/src/Library/Economy/Currency/FrankfurterExchange.php index 8aa7a54..5774d59 100644 --- a/src/Library/Economy/Currency/FrankfurterExchange.php +++ b/src/Library/Economy/Currency/FrankfurterExchange.php @@ -34,7 +34,7 @@ class FrankfurterExchange implements ExchangeInterface } public function __construct( - HttpClientInterface $httpClient + HttpClientInterface $httpClient, ) { $response = $httpClient->request('GET', self::ENDPOINT); $data = $response->toArray(); From f8c8db1f5c291c042a6d56ac72da24bae5f029b8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 09:30:57 +0200 Subject: [PATCH 133/203] Fix md titles for composite description --- src/Benzina/ProjectsPump.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 9c76b7b..390f097 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -111,13 +111,13 @@ class ProjectsPump implements PumpInterface $description = $record['description']; - $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['about'] : ''); + $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['about'] : ''); $description .= \sprintf("\n%s", $record['about']); - $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['motivation'] : ''); + $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['motivation'] : ''); $description .= \sprintf("\n%s", $record['motivation']); - $description .= \sprintf("\n##%s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['related'] : ''); + $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['related'] : ''); $description .= \sprintf("\n%s", $record['related']); return $description; From d2c74702f344ea310f111d8cc2f382596c5d1d2c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 14:13:33 +0200 Subject: [PATCH 134/203] Minor updates on project and user entities --- src/Entity/Project/Project.php | 11 ++++++++++ src/Entity/User/User.php | 38 ---------------------------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/src/Entity/Project/Project.php b/src/Entity/Project/Project.php index 0f7d366..a34b5dc 100644 --- a/src/Entity/Project/Project.php +++ b/src/Entity/Project/Project.php @@ -25,6 +25,7 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; #[MapProvider(EntityMapProvider::class)] +#[ORM\Index(fields: ['migratedId'])] #[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project implements UserOwnedInterface, AccountingOwnerInterface, LocalizedEntityInterface { @@ -396,6 +397,16 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this->updates; } + /** + * @param Collection $updates + */ + public function setUpdates(Collection $updates): static + { + $this->updates = $updates; + + return $this; + } + public function addUpdate(Update $update): static { if (!$this->updates->contains($update)) { diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 34c03f9..3e12ccc 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -5,7 +5,6 @@ namespace App\Entity\User; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Project\Project; -use App\Entity\Project\Support; use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; @@ -112,12 +111,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $avatar = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: Support::class, mappedBy: 'owner')] - private Collection $supports; - public function __construct() { $this->accounting = Accounting::of($this); @@ -128,7 +121,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account $this->emailConfirmed = false; $this->active = false; - $this->supports = new ArrayCollection(); } public function getId(): ?int @@ -377,34 +369,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - - /** - * @return Collection - */ - public function getSupports(): Collection - { - return $this->supports; - } - - public function addSupport(Support $support): static - { - if (!$this->supports->contains($support)) { - $this->supports->add($support); - $support->setOwner($this); - } - - return $this; - } - - public function removeSupport(Support $support): static - { - if ($this->supports->removeElement($support)) { - // set the owning side to null (unless already changed) - if ($support->getOwner() === $this) { - $support->setOwner(null); - } - } - - return $this; - } } From 8aa962fec289286336c7d7796b4c47f5594676aa Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 14:13:54 +0200 Subject: [PATCH 135/203] Fix description composition on projects pumping --- src/Benzina/ProjectsPump.php | 55 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 390f097..eecd8d7 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -11,9 +11,11 @@ use App\Entity\Project\ProjectVideo; use App\Entity\Project\Update; use App\Entity\Territory; use App\Entity\User\User; +use App\Repository\Project\ProjectRepository; use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; use App\Service\Project\TerritoryService; +use Doctrine\Common\Collections\ArrayCollection; use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; @@ -27,6 +29,7 @@ class ProjectsPump implements PumpInterface use ProjectsPumpTrait; public function __construct( + private ProjectRepository $projectRepository, private UserRepository $userRepository, private TerritoryService $territoryService, private EmbedService $embedService, @@ -57,7 +60,11 @@ class ProjectsPump implements PumpInterface return; } - $project = new Project(); + $project = $this->getProject($record); + if ($project === null) { + $project = new Project(); + } + $project->setSlug($record['id']); $project->setCategory($this->getProjectCategory($record)); $project->setTerritory($this->getProjectTerritory($record)); @@ -68,11 +75,12 @@ class ProjectsPump implements PumpInterface $project->setMigratedId($record['id']); $project->setDateCreated(new \DateTime($record['created'])); $project->setDateUpdated(new \DateTime()); + $project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context))); - $project->setTranslatableLocale($record['lang']); - $project->setTitle($record['name']); - $project->setSubtitle($record['subtitle']); - $project->setDescription($this->getProjectDescription($record)); + $conf = $this->getProjectConf($project, $context); + + $project->setDeadline($this->getProjectDeadline($conf)); + $project->setCalendar($this->getProjectCalendar($record)); $localizations = $this->getProjectLocalizations($project, $context); $translations = $this->entityManager->getRepository(Translation::class); @@ -87,19 +95,18 @@ class ProjectsPump implements PumpInterface ; } - $updates = $this->getProjectUpdates($project, $context); - foreach ($updates as $update) { - $project->addUpdate($update); - } - - $conf = $this->getProjectConf($project, $context); - - $project->setDeadline($this->getProjectDeadline($conf)); - $project->setCalendar($this->getProjectCalendar($record)); + $project->setTitle($record['name']); + $project->setSubtitle($record['subtitle']); + $project->setDescription($this->getProjectDescription($record)); $this->persist($project, $context); } + private function getProject(array $record): ?Project + { + return $this->projectRepository->findOneBy(['migratedId' => $record['id']]); + } + private function getProjectOwner(array $record): ?User { return $this->userRepository->findOneBy(['migratedId' => $record['owner']]); @@ -107,17 +114,27 @@ class ProjectsPump implements PumpInterface private function getProjectDescription(array $record): string { - $hasTitles = \array_key_exists($record['lang'], self::PROJECT_DESC_TITLES); + $lang = $record['lang']; + $hasTitles = \array_key_exists($lang, self::PROJECT_DESC_TITLES); $description = $record['description']; - $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['about'] : ''); + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['about']); + } + $description .= \sprintf("\n%s", $record['about']); - $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['motivation'] : ''); - $description .= \sprintf("\n%s", $record['motivation']); + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['motivation']); + } + + $description .= \sprintf("\n\n%s", $record['motivation']); + + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['related']); + } - $description .= \sprintf("\n## %s", $hasTitles ? self::PROJECT_DESC_TITLES[$record['lang']]['related'] : ''); $description .= \sprintf("\n%s", $record['related']); return $description; From ac231c8a6216b5cda2af338c866aa2cb743deb20 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 16:05:16 +0200 Subject: [PATCH 136/203] Pump localizations of project rewards --- src/Benzina/ProjectsRewardsPump.php | 27 +++++++++++++++++++++++++++ src/Entity/Project/Reward.php | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Benzina/ProjectsRewardsPump.php b/src/Benzina/ProjectsRewardsPump.php index cf29234..8c53a22 100644 --- a/src/Benzina/ProjectsRewardsPump.php +++ b/src/Benzina/ProjectsRewardsPump.php @@ -7,6 +7,7 @@ use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\Reward; use App\Repository\Project\ProjectRepository; +use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -14,6 +15,7 @@ use Goteo\Benzina\Pump\PumpInterface; class ProjectsRewardsPump implements PumpInterface { use ArrayPumpTrait; + use DatabasePumpTrait; use DoctrinePumpTrait; private const REWARD_KEYS = [ @@ -67,6 +69,8 @@ class ProjectsRewardsPump implements PumpInterface $reward = new Reward(); $reward->setTranslatableLocale($project->getLocales()[0]); $reward->setProject($project); + $reward->setMigrated(true); + $reward->setMigratedId($record['id']); $reward->setTitle($record['reward']); $reward->setDescription($record['description'] ?? $record['reward']); $reward->setMoney(new Money($record['amount'] * 100, 'EUR')); @@ -74,6 +78,18 @@ class ProjectsRewardsPump implements PumpInterface $reward->setUnitsTotal($record['units'] ?? 0); $reward->setUnitsAvailable($record['units'] ?? 0); + $localizations = $this->getRewardLocalizations($reward, $context); + $translations = $this->entityManager->getRepository(Translation::class); + foreach ($localizations as $localization) { + $locale = $localization['lang']; + + $reward->addLocale($locale); + $translations + ->translate($reward, 'title', $locale, $localization['reward'] ?? $record['reward']) + ->translate($reward, 'description', $locale, $localization['description'] ?? $record['description']) + ; + } + $this->persist($reward, $context); } @@ -93,4 +109,15 @@ class ProjectsRewardsPump implements PumpInterface return BudgetItemType::Infrastructure; } } + + private function getRewardLocalizations(Reward $reward, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `reward_lang` l WHERE l.id = :reward' + ); + + $query->execute(['reward' => $reward->getMigratedId()]); + + return $query->fetchAll(); + } } diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index 27cbad5..dc3f60d 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -2,8 +2,10 @@ namespace App\Entity\Project; +use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Money; use App\Entity\Trait\LocalizedEntityTrait; +use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\RewardRepository; use AutoMapper\Attribute\MapProvider; @@ -19,8 +21,9 @@ use Gedmo\Mapping\Annotation as Gedmo; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'project_reward')] #[ORM\Entity(repositoryClass: RewardRepository::class)] -class Reward +class Reward implements LocalizedEntityInterface { + use MigratedEntity; use LocalizedEntityTrait; #[ORM\Id] From 098db8e3bef5e9456a5330dfd01c65846be17444 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 16:18:52 +0200 Subject: [PATCH 137/203] Pump localizations of project budget items --- src/Benzina/ProjectsBudgetPump.php | 27 +++++++++++++++++++++++++++ src/Benzina/ProjectsPump.php | 1 + src/Entity/Project/BudgetItem.php | 2 ++ 3 files changed, 30 insertions(+) diff --git a/src/Benzina/ProjectsBudgetPump.php b/src/Benzina/ProjectsBudgetPump.php index e2805f8..f4702f8 100644 --- a/src/Benzina/ProjectsBudgetPump.php +++ b/src/Benzina/ProjectsBudgetPump.php @@ -8,6 +8,7 @@ use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Repository\Project\ProjectRepository; +use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -15,6 +16,7 @@ use Goteo\Benzina\Pump\PumpInterface; class ProjectsBudgetPump implements PumpInterface { use ArrayPumpTrait; + use DatabasePumpTrait; use DoctrinePumpTrait; private const COST_KEYS = [ @@ -55,6 +57,8 @@ class ProjectsBudgetPump implements PumpInterface } $budgetItem = new BudgetItem(); + $budgetItem->setMigrated(true); + $budgetItem->setMigratedId($record['id']); $budgetItem->setTranslatableLocale($project->getLocales()[0]); $budgetItem->setProject($project); $budgetItem->setType($this->getCostType($record)); @@ -63,6 +67,18 @@ class ProjectsBudgetPump implements PumpInterface $budgetItem->setMoney(new Money($record['amount'] * 100, 'EUR')); $budgetItem->setDeadline($this->getDeadline($record)); + $localizations = $this->getCostLocalizations($budgetItem, $context); + $translations = $this->entityManager->getRepository(Translation::class); + foreach ($localizations as $localization) { + $locale = $localization['lang']; + + $budgetItem->addLocale($locale); + $translations + ->translate($budgetItem, 'title', $locale, $localization['cost'] ?? $record['cost']) + ->translate($budgetItem, 'description', $locale, $localization['description'] ?? $record['description']) + ; + } + $this->persist($budgetItem, $context); } @@ -91,4 +107,15 @@ class ProjectsBudgetPump implements PumpInterface return ProjectDeadline::Optimum; } + + private function getCostLocalizations(BudgetItem $budgetItem, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `cost_lang` l WHERE l.id = :cost' + ); + + $query->execute(['cost' => $budgetItem->getMigratedId()]); + + return $query->fetchAll(); + } } diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index eecd8d7..50e0720 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -75,6 +75,7 @@ class ProjectsPump implements PumpInterface $project->setMigratedId($record['id']); $project->setDateCreated(new \DateTime($record['created'])); $project->setDateUpdated(new \DateTime()); + $project->setTranslatableLocale($record['lang']); $project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context))); $conf = $this->getProjectConf($project, $context); diff --git a/src/Entity/Project/BudgetItem.php b/src/Entity/Project/BudgetItem.php index 73ab6b9..4933eb5 100644 --- a/src/Entity/Project/BudgetItem.php +++ b/src/Entity/Project/BudgetItem.php @@ -5,6 +5,7 @@ namespace App\Entity\Project; use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Money; use App\Entity\Trait\LocalizedEntityTrait; +use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\BudgetItemRepository; use AutoMapper\Attribute\MapProvider; @@ -17,6 +18,7 @@ use Gedmo\Mapping\Annotation as Gedmo; #[ORM\Entity(repositoryClass: BudgetItemRepository::class)] class BudgetItem implements LocalizedEntityInterface { + use MigratedEntity; use LocalizedEntityTrait; #[ORM\Id] From b42962c3eab9d5163912896264f4bc88fd348c1f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 16:58:21 +0200 Subject: [PATCH 138/203] Update checkouts pumping --- src/Benzina/CheckoutsPump.php | 27 ++++++++++++++++++++------- src/Entity/Gateway/Checkout.php | 2 -- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index 4b226f9..3acafbd 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -2,6 +2,7 @@ namespace App\Benzina; +use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Gateway\Tracking; @@ -40,6 +41,7 @@ class CheckoutsPump implements PumpInterface public const CHARGE_TITLE_POOL = 'Pago en Goteo v3 - Carga de monedero'; public const CHARGE_TITLE_TIP = 'Pago en Goteo v3 - Propina a la plataforma'; + private const PLATFORM_RETURN_URL = 'https://goteo.org'; private const PLATFORM_TIPJAR_NAME = 'platform'; private const MAX_INT = 2147483647; @@ -81,23 +83,26 @@ class CheckoutsPump implements PumpInterface $project = $this->getProject($record); $tipjar = $this->getPlatformTipjar(); + $invested = new \DateTime($record['invested']); $checkout = new Checkout(); + $checkout->setMigrated(true); + $checkout->setMigratedId($record['id']); + $checkout->setDateCreated($invested); + $checkout->setDateUpdated(new \DateTime()); + $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); $checkout->setGatewayName($this->getCheckoutGateway($record)); + $checkout->setReturnUrl(self::PLATFORM_RETURN_URL); foreach ($this->getCheckoutTrackings($record) as $tracking) { $checkout->addTracking($tracking); } - $checkout->setMigrated(true); - $checkout->setMigratedId($record['id']); - - $checkout->setDateCreated(new \DateTime($record['invested'])); - $checkout->setDateUpdated(new \DateTime()); - $charge = new Charge(); + $charge->setDateCreated($invested); + $charge->setDateUpdated(new \DateTime()); $charge->setType($this->getChargeType($record)); $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); @@ -122,7 +127,15 @@ class CheckoutsPump implements PumpInterface } if ($checkout->getStatus() === CheckoutStatus::Charged) { - $checkout = $this->checkoutService->chargeCheckout($checkout); + foreach ($checkout->getCharges() as $charge) { + $transaction = new Transaction(); + $transaction->setDateCreated($invested); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $charge->addTransaction($transaction); + } } $this->persist($checkout, $context); diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 4a35d8b..0b44f56 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -76,8 +76,6 @@ class Checkout /** * The address to where the user must be redirected to. */ - #[Assert\NotBlank()] - #[Assert\Url()] #[ORM\Column(type: Types::TEXT)] private ?string $returnUrl = null; From da5a4d0c585f532b705c17a4e6a3a9df2b6ca849 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 17:22:48 +0200 Subject: [PATCH 139/203] Invest amounts were converted to EUR in v3 --- src/Benzina/CheckoutsPump.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index 3acafbd..ad24c5f 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -45,6 +45,7 @@ class CheckoutsPump implements PumpInterface private const PLATFORM_TIPJAR_NAME = 'platform'; private const MAX_INT = 2147483647; + private const CURRENCY = 'EUR'; public function __construct( private UserRepository $userRepository, @@ -104,7 +105,7 @@ class CheckoutsPump implements PumpInterface $charge->setDateCreated($invested); $charge->setDateUpdated(new \DateTime()); $charge->setType($this->getChargeType($record)); - $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); + $charge->setMoney($this->getChargeMoney($record['amount'], self::CURRENCY)); if ($project === null) { $charge->setTitle(self::CHARGE_TITLE_POOL); @@ -120,7 +121,7 @@ class CheckoutsPump implements PumpInterface $tip = new Charge(); $tip->setType(ChargeType::Single); $tip->setTitle(self::CHARGE_TITLE_TIP); - $tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency'])); + $tip->setMoney($this->getChargeMoney($record['donate_amount'], self::CURRENCY)); $tip->setTarget($tipjar->getAccounting()); $checkout->addCharge($tip); From 7efbf926b64b1345782b512120cbac94c8c73031 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 3 Sep 2025 20:12:17 +0200 Subject: [PATCH 140/203] Fix status processing of charges in checkouts pumping --- src/Benzina/CheckoutsPump.php | 74 +++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index ad24c5f..6a77c70 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -10,6 +10,7 @@ use App\Entity\Money; use App\Entity\Project\Project; use App\Entity\Tipjar; use App\Entity\User\User; +use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\Gateway\CashGateway; @@ -104,6 +105,7 @@ class CheckoutsPump implements PumpInterface $charge = new Charge(); $charge->setDateCreated($invested); $charge->setDateUpdated(new \DateTime()); + $charge->setStatus($this->getChargeStatus($record)); $charge->setType($this->getChargeType($record)); $charge->setMoney($this->getChargeMoney($record['amount'], self::CURRENCY)); @@ -119,6 +121,9 @@ class CheckoutsPump implements PumpInterface if ($record['donate_amount'] > 0) { $tip = new Charge(); + $tip->setDateCreated($invested); + $tip->setDateUpdated(new \DateTime()); + $tip->setStatus($this->getChargeStatus($record)); $tip->setType(ChargeType::Single); $tip->setTitle(self::CHARGE_TITLE_TIP); $tip->setMoney($this->getChargeMoney($record['donate_amount'], self::CURRENCY)); @@ -127,16 +132,30 @@ class CheckoutsPump implements PumpInterface $checkout->addCharge($tip); } - if ($checkout->getStatus() === CheckoutStatus::Charged) { - foreach ($checkout->getCharges() as $charge) { - $transaction = new Transaction(); - $transaction->setDateCreated($invested); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); - - $charge->addTransaction($transaction); + foreach ($checkout->getCharges() as $charge) { + if (!\in_array($charge->getStatus(), [ChargeStatus::Charged, ChargeStatus::Refunded])) { + continue; } + + $transaction = new Transaction(); + $transaction->setDateCreated($invested); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $charge->addTransaction($transaction); + + if ($charge->getStatus() !== ChargeStatus::Refunded) { + continue; + } + + $returned = new Transaction(); + $returned->setDateCreated(new \DateTime($record['returned'])); + $returned->setMoney($charge->getMoney()); + $returned->setOrigin($charge->getTarget()); + $returned->setTarget($checkout->getOrigin()); + + $charge->addTransaction($returned); } $this->persist($checkout, $context); @@ -193,13 +212,42 @@ class CheckoutsPump implements PumpInterface return new Money($amount, $currency); } + private function getChargeStatus(array $record): ChargeStatus + { + switch ($record['status']) { + case 0: + case 1: + case 3: + case 7: + if ($record['issue'] === 1) { + return ChargeStatus::InPending; + } + + return ChargeStatus::Charged; + case 2: + case 4: + case 6: + return ChargeStatus::Refunded; + default: + return ChargeStatus::InPending; + } + } + private function getCheckoutStatus(array $record): CheckoutStatus { - if ($record['status'] < 1) { - return CheckoutStatus::InPending; - } + switch ($record['status']) { + case 0: + case 1: + case 3: + case 7: + if ($record['issue'] === 1) { + return CheckoutStatus::InPending; + } - return CheckoutStatus::Charged; + return CheckoutStatus::Charged; + default: + return CheckoutStatus::InPending; + } } private function getCheckoutGateway(array $record): string From 690833e7c8f60e2e6e7cc8b0dbd10b30b925ab86 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 12:25:52 +0200 Subject: [PATCH 141/203] Fix bad array access on unitialized projectId key --- src/EventListener/GatewayCheckoutSupportsListener.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 337e98d..6cd6717 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -91,7 +91,10 @@ class GatewayCheckoutSupportsListener $projectId = $project->getId(); $projects[$projectId] = $project; - $transactionsByProject[$projectId] = [...$transactionsByProject[$projectId], ...$charge->getTransactions()]; + $transactionsByProject[$projectId] = [ + ...$transactionsByProject[$projectId] ?? [], + ...$charge->getTransactions() + ]; } $supports = []; From 69b386c4d5d59f409a655915e3395984bd44bb96 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 12:29:07 +0200 Subject: [PATCH 142/203] Add trailing comma by php-cs-fixer --- src/EventListener/GatewayCheckoutSupportsListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 6cd6717..fbffddb 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -93,7 +93,7 @@ class GatewayCheckoutSupportsListener $projects[$projectId] = $project; $transactionsByProject[$projectId] = [ ...$transactionsByProject[$projectId] ?? [], - ...$charge->getTransactions() + ...$charge->getTransactions(), ]; } From 1140a87e2133431139526a81253d66aa593bc97a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 15:14:45 +0200 Subject: [PATCH 143/203] Fix Support not being saved to db by listener --- .../GatewayCheckoutSupportsListener.php | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index fbffddb..a43956a 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -8,51 +8,35 @@ use App\Entity\Gateway\Checkout; use App\Entity\Project\Project; use App\Entity\Project\Support; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; -use Doctrine\ORM\Event\PostUpdateEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\PreFlushEventArgs; use Doctrine\ORM\Events; #[AsEntityListener( - event: Events::preUpdate, - method: 'preUpdate', - entity: Checkout::class -)] -#[AsEntityListener( - event: Events::postUpdate, - method: 'postUpdate', + event: Events::preFlush, + method: 'preFlush', entity: Checkout::class )] class GatewayCheckoutSupportsListener { - /** @var array */ - private array $supports; - - public function preUpdate(Checkout $checkout, PreUpdateEventArgs $args): void + public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void { - if (!$args->hasChangedField('status')) { + if (!$checkout->isCharged()) { return; } - if ($checkout->isCharged()) { - $this->supports = $this->prepareSupports($checkout, $args->getObjectManager()); + $supports = $this->prepareSupports($checkout); + + /** @var EntityManagerInterface */ + $em = $args->getObjectManager(); + $uow = $em->getUnitOfWork(); + + foreach ($supports as $support) { + $em->persist($support); + $uow->computeChangeSet($em->getClassMetadata(Support::class), $support); } } - public function postUpdate(Checkout $checkout, PostUpdateEventArgs $args): void - { - if (empty($this->supports)) { - return; - } - - foreach ($this->supports as $key => $support) { - $args->getObjectManager()->persist($support); - - unset($this->supports[$key]); - } - - $args->getObjectManager()->flush(); - } - /** * Create ProjectSupport for the given data. */ From e4d56b8138ff7010e5de64b998e17a04301e04e8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 15:19:59 +0200 Subject: [PATCH 144/203] Add unique support by project and origin constraint --- src/Entity/Project/Support.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index 9d334dc..2c1a4bc 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -12,8 +12,10 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[MapProvider(EntityMapProvider::class)] -#[ORM\Table(name: 'project_support')] #[ORM\Entity(repositoryClass: SupportRepository::class)] +#[ORM\Table(name: 'project_support', uniqueConstraints: [ + new ORM\UniqueConstraint(fields: ['project', 'origin']) +])] class Support { #[ORM\Id] From a784d12ad3a85b37d652aefe67e7cefc7e18de10 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 15:31:11 +0200 Subject: [PATCH 145/203] Only create supports for checkouts going into charge status --- .../GatewayCheckoutSupportsListener.php | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index a43956a..2249e88 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -21,19 +21,27 @@ class GatewayCheckoutSupportsListener { public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void { - if (!$checkout->isCharged()) { - return; - } - - $supports = $this->prepareSupports($checkout); - /** @var EntityManagerInterface */ $em = $args->getObjectManager(); $uow = $em->getUnitOfWork(); - foreach ($supports as $support) { - $em->persist($support); - $uow->computeChangeSet($em->getClassMetadata(Support::class), $support); + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof Checkout) { + continue; + } + + $changes = $uow->getEntityChangeSet($entity); + + if (!isset($changes['status']) || !$entity->isCharged()) { + continue; + } + + $supports = $this->prepareSupports($entity); + + foreach ($supports as $support) { + $em->persist($support); + $uow->computeChangeSet($em->getClassMetadata(Support::class), $support); + } } } From e65f0746ddb597c685307d1e7c0cd831545989be Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 15:34:40 +0200 Subject: [PATCH 146/203] Unify joint trxs column name --- src/Entity/Gateway/Charge.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index 0ca1261..8bf56f4 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -77,7 +77,7 @@ class Charge /** * @var Collection */ - #[ORM\JoinTable(name: 'checkout_charge_transaction')] + #[ORM\JoinTable(name: 'checkout_charge_trxs')] #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; From 27a986f21d04a181f5be76a0cbfda625f53b8352 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 15:41:18 +0200 Subject: [PATCH 147/203] Get existing supports for same project and origin --- src/Entity/Project/Support.php | 4 ++-- .../GatewayCheckoutSupportsListener.php | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index 2c1a4bc..d37c05c 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM; #[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: SupportRepository::class)] #[ORM\Table(name: 'project_support', uniqueConstraints: [ - new ORM\UniqueConstraint(fields: ['project', 'origin']) + new ORM\UniqueConstraint(fields: ['project', 'origin']), ])] class Support { @@ -37,7 +37,7 @@ class Support #[ORM\JoinTable(name: 'project_support_trxs')] #[ORM\JoinColumn(name: 'support_id', referencedColumnName: 'id')] #[ORM\InverseJoinColumn(name: 'transaction_id', referencedColumnName: 'id')] - #[ORM\ManyToMany(targetEntity: Transaction::class)] + #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; #[ORM\Column] diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 2249e88..5d11ddd 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -7,6 +7,7 @@ use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Project\Project; use App\Entity\Project\Support; +use App\Repository\Project\SupportRepository; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PreFlushEventArgs; @@ -19,6 +20,10 @@ use Doctrine\ORM\Events; )] class GatewayCheckoutSupportsListener { + public function __construct( + private SupportRepository $supportRepository, + ) {} + public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void { /** @var EntityManagerInterface */ @@ -50,7 +55,15 @@ class GatewayCheckoutSupportsListener */ private function createSupport(Project $project, Accounting $origin, array $transactions): Support { - $projectSupport = new Support(); + $projectSupport = $this->supportRepository->findOneBy([ + 'project' => $project->getId(), + 'origin' => $origin->getId(), + ]); + + if (!$projectSupport) { + $projectSupport = new Support(); + } + $projectSupport->setProject($project); $projectSupport->setOrigin($origin); $projectSupport->setAnonymous(false); From 6de675ce86fc425d2c74a07bcbdfed910e73f102 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 17:39:25 +0200 Subject: [PATCH 148/203] Set cors allow credentials true --- config/packages/nelmio_cors.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml index c766508..7b20162 100644 --- a/config/packages/nelmio_cors.yaml +++ b/config/packages/nelmio_cors.yaml @@ -4,6 +4,7 @@ nelmio_cors: allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization'] + allow_credentials: true expose_headers: ['Link'] max_age: 3600 paths: From 06643724f6b62c45be5193cc5ad2d9d3534596d7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 19:19:59 +0200 Subject: [PATCH 149/203] Set and remove authToken cookie on token post/delete --- .../AuthorizationTokenSubscriber.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/EventSubscriber/AuthorizationTokenSubscriber.php diff --git a/src/EventSubscriber/AuthorizationTokenSubscriber.php b/src/EventSubscriber/AuthorizationTokenSubscriber.php new file mode 100644 index 0000000..864e645 --- /dev/null +++ b/src/EventSubscriber/AuthorizationTokenSubscriber.php @@ -0,0 +1,82 @@ + ['onView', EventPriorities::POST_WRITE], + KernelEvents::RESPONSE => ['onResponse', EventPriorities::POST_RESPOND], + ]; + } + + private function getCookieTtl(): \DateTimeInterface + { + return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', self::COOKIE_TTL))); + } + + private function getCookie(string $tokenValue) + { + return new Cookie( + name: self::COOKIE_NAME, + value: $tokenValue, + expire: $this->getCookieTtl(), + path: '/', + secure: false, + httpOnly: true, + sameSite: 'Lax' + ); + } + + public function onView(ViewEvent $event): void + { + $resourceClass = $event->getRequest()->attributes->get('_api_resource_class'); + + if ($resourceClass !== UserTokenApiResource::class) { + return; + } + + $event->getRequest()->attributes->set('authTokenMethod', $event->getRequest()->getMethod()); + + if ($event->getRequest()->getMethod() === Request::METHOD_POST) { + $token = $event->getControllerResult(); + + $event->getRequest()->attributes->set('authTokenValue', $token->token); + } + } + + public function onResponse(ResponseEvent $event): void + { + $token = $event->getRequest()->attributes->get('authTokenValue'); + $method = $event->getRequest()->attributes->get('authTokenMethod'); + + if (!$method) { + return; + } + + switch ($method) { + case Request::METHOD_DELETE: + $event->getResponse()->headers->clearCookie(self::COOKIE_NAME); + break; + case Request::METHOD_POST: + $event->getResponse()->headers->setCookie($this->getCookie($token)); + break; + } + } +} From fe247d666f653407afcb44461257e3e742eb153d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 19:38:07 +0200 Subject: [PATCH 150/203] Centralize cookie consts in AuthServices --- src/EventSubscriber/AuthorizationTokenSubscriber.php | 10 ++++------ src/Service/Auth/AuthService.php | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/EventSubscriber/AuthorizationTokenSubscriber.php b/src/EventSubscriber/AuthorizationTokenSubscriber.php index 864e645..742ecdd 100644 --- a/src/EventSubscriber/AuthorizationTokenSubscriber.php +++ b/src/EventSubscriber/AuthorizationTokenSubscriber.php @@ -4,6 +4,7 @@ namespace App\EventSubscriber; use ApiPlatform\Symfony\EventListener\EventPriorities; use App\ApiResource\User\UserTokenApiResource; +use App\Service\Auth\AuthService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; @@ -13,9 +14,6 @@ use Symfony\Component\HttpKernel\KernelEvents; final class AuthorizationTokenSubscriber implements EventSubscriberInterface { - private const COOKIE_NAME = 'authToken'; - private const COOKIE_TTL = 86400; - public function __construct() {} public static function getSubscribedEvents() @@ -28,13 +26,13 @@ final class AuthorizationTokenSubscriber implements EventSubscriberInterface private function getCookieTtl(): \DateTimeInterface { - return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', self::COOKIE_TTL))); + return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', AuthService::AUTH_COOKIE_TTL))); } private function getCookie(string $tokenValue) { return new Cookie( - name: self::COOKIE_NAME, + name: AuthService::AUTH_COOKIE_NAME, value: $tokenValue, expire: $this->getCookieTtl(), path: '/', @@ -72,7 +70,7 @@ final class AuthorizationTokenSubscriber implements EventSubscriberInterface switch ($method) { case Request::METHOD_DELETE: - $event->getResponse()->headers->clearCookie(self::COOKIE_NAME); + $event->getResponse()->headers->clearCookie(AuthService::AUTH_COOKIE_NAME); break; case Request::METHOD_POST: $event->getResponse()->headers->setCookie($this->getCookie($token)); diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 1bd1d47..4cbbe3e 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -9,6 +9,9 @@ use Symfony\Bundle\SecurityBundle\Security; class AuthService { + public const AUTH_COOKIE_NAME = 'authToken'; + public const AUTH_COOKIE_TTL = 86400; + private const TOKEN_HASH_ALGO = 'sha256'; private array $config; From c4e0d6394dae0168f1c799610dde2f98cc9f2822 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 19:40:05 +0200 Subject: [PATCH 151/203] Add Cookie based authenticator --- config/packages/security.yaml | 2 + src/Security/CookieAuthenticator.php | 56 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/Security/CookieAuthenticator.php diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 191ba3a..92747af 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -25,6 +25,8 @@ security: access_token: token_handler: App\Security\AccessTokenHandler + custom_authenticators: + - App\Security\CookieAuthenticator # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/src/Security/CookieAuthenticator.php b/src/Security/CookieAuthenticator.php new file mode 100644 index 0000000..6ad4757 --- /dev/null +++ b/src/Security/CookieAuthenticator.php @@ -0,0 +1,56 @@ +cookies->has(AuthService::AUTH_COOKIE_NAME); + } + + public function authenticate(Request $request): Passport + { + $token = $request->cookies->get(AuthService::AUTH_COOKIE_NAME); + + if (!$token) { + throw new AuthenticationException(); + } + + return new SelfValidatingPassport($this->accessTokenHandler->getUserBadgeFrom($token)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), + ], Response::HTTP_UNAUTHORIZED); + } +} From c32c08c74b7ba769dd7e3d6120af8ed22770fb23 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 4 Sep 2025 19:44:25 +0200 Subject: [PATCH 152/203] Centralize cookie generation on AuthService --- .../AuthorizationTokenSubscriber.php | 25 +++---------------- src/Security/CookieAuthenticator.php | 2 +- src/Service/Auth/AuthService.php | 19 ++++++++++++++ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/EventSubscriber/AuthorizationTokenSubscriber.php b/src/EventSubscriber/AuthorizationTokenSubscriber.php index 742ecdd..1c9541f 100644 --- a/src/EventSubscriber/AuthorizationTokenSubscriber.php +++ b/src/EventSubscriber/AuthorizationTokenSubscriber.php @@ -6,7 +6,6 @@ use ApiPlatform\Symfony\EventListener\EventPriorities; use App\ApiResource\User\UserTokenApiResource; use App\Service\Auth\AuthService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; @@ -14,7 +13,9 @@ use Symfony\Component\HttpKernel\KernelEvents; final class AuthorizationTokenSubscriber implements EventSubscriberInterface { - public function __construct() {} + public function __construct( + private AuthService $authService, + ) {} public static function getSubscribedEvents() { @@ -24,24 +25,6 @@ final class AuthorizationTokenSubscriber implements EventSubscriberInterface ]; } - private function getCookieTtl(): \DateTimeInterface - { - return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', AuthService::AUTH_COOKIE_TTL))); - } - - private function getCookie(string $tokenValue) - { - return new Cookie( - name: AuthService::AUTH_COOKIE_NAME, - value: $tokenValue, - expire: $this->getCookieTtl(), - path: '/', - secure: false, - httpOnly: true, - sameSite: 'Lax' - ); - } - public function onView(ViewEvent $event): void { $resourceClass = $event->getRequest()->attributes->get('_api_resource_class'); @@ -73,7 +56,7 @@ final class AuthorizationTokenSubscriber implements EventSubscriberInterface $event->getResponse()->headers->clearCookie(AuthService::AUTH_COOKIE_NAME); break; case Request::METHOD_POST: - $event->getResponse()->headers->setCookie($this->getCookie($token)); + $event->getResponse()->headers->setCookie($this->authService->generateCookie($token)); break; } } diff --git a/src/Security/CookieAuthenticator.php b/src/Security/CookieAuthenticator.php index 6ad4757..7e8e91f 100644 --- a/src/Security/CookieAuthenticator.php +++ b/src/Security/CookieAuthenticator.php @@ -18,7 +18,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPasspor class CookieAuthenticator extends AbstractAuthenticator { public function __construct( - private AccessTokenHandler $accessTokenHandler + private AccessTokenHandler $accessTokenHandler, ) {} /** diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 4cbbe3e..0a17a9a 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -6,6 +6,7 @@ use App\Entity\User\User; use App\Entity\User\UserToken; use App\Repository\User\UserRepository; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Cookie; class AuthService { @@ -30,6 +31,24 @@ class AuthService return $this->config; } + private function getCookieTtl(): \DateTimeInterface + { + return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', AuthService::AUTH_COOKIE_TTL))); + } + + public function generateCookie(string $token): Cookie + { + return new Cookie( + name: AuthService::AUTH_COOKIE_NAME, + value: $token, + expire: $this->getCookieTtl(), + path: '/', + secure: true, + httpOnly: true, + sameSite: 'Strict' + ); + } + public function generateUserToken(User $user, AuthTokenType $type): UserToken { $token = new UserToken(); From 2a162b99fa4b2941d9aa10c11e6d5b892eb3c126 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 5 Sep 2025 15:01:37 +0200 Subject: [PATCH 153/203] Update support entity --- src/Entity/Project/Support.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index d37c05c..7d86673 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -9,6 +9,7 @@ use App\Repository\Project\SupportRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[MapProvider(EntityMapProvider::class)] @@ -41,9 +42,9 @@ class Support private Collection $transactions; #[ORM\Column] - private ?bool $anonymous = null; + private bool $anonymous = false; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $message = null; public function __construct() @@ -104,7 +105,7 @@ class Support return $this; } - public function isAnonymous(): ?bool + public function isAnonymous(): bool { return $this->anonymous; } From f3fb485ffbe2b2b0e943f4f508a9bf4a6b8215da Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 5 Sep 2025 20:50:39 +0200 Subject: [PATCH 154/203] Pump ProjectSupports along with invests --- src/Benzina/CheckoutsPump.php | 69 +++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index 6a77c70..a68e6f6 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -8,6 +8,7 @@ use App\Entity\Gateway\Checkout; use App\Entity\Gateway\Tracking; use App\Entity\Money; use App\Entity\Project\Project; +use App\Entity\Project\Support; use App\Entity\Tipjar; use App\Entity\User\User; use App\Gateway\ChargeStatus; @@ -20,6 +21,7 @@ use App\Gateway\Paypal\PaypalGateway; use App\Gateway\Stripe\StripeGateway; use App\Gateway\Wallet\WalletGateway; use App\Repository\Project\ProjectRepository; +use App\Repository\Project\SupportRepository; use App\Repository\TipjarRepository; use App\Repository\User\UserRepository; use App\Service\Gateway\CheckoutService; @@ -30,6 +32,7 @@ use Goteo\Benzina\Pump\PumpInterface; class CheckoutsPump implements PumpInterface { use ArrayPumpTrait; + use DatabasePumpTrait; use DoctrinePumpTrait; use CheckoutsPumpTrait; @@ -51,6 +54,7 @@ class CheckoutsPump implements PumpInterface public function __construct( private UserRepository $userRepository, private ProjectRepository $projectRepository, + private SupportRepository $supportRepository, private TipjarRepository $tipjarRepository, private CheckoutService $checkoutService, ) {} @@ -145,6 +149,17 @@ class CheckoutsPump implements PumpInterface $charge->addTransaction($transaction); + if ($project) { + $support = $this->getSupport($charge); + $support->setProject($project); + $support->setOrigin($checkout->getOrigin()); + $support->addTransaction($transaction); + $support->setAnonymous($this->getSupportAnon($record, $support)); + $support->setMessage($this->getSupportMessage($record, $support, $context)); + + $this->entityManager->persist($support); + } + if ($charge->getStatus() !== ChargeStatus::Refunded) { continue; } @@ -175,6 +190,49 @@ class CheckoutsPump implements PumpInterface return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); } + private function getSupport(Charge $charge): Support + { + $support = $this->supportRepository->findOneBy([ + 'origin' => $charge->getCheckout()->getOrigin()->getId(), + 'project' => $charge->getTarget()->getOwner()->getId(), + ]); + + if ($support) { + return $support; + } + + return new Support(); + } + + private function getSupportAnon(array $record, Support $support): bool + { + if ($record['anonymous'] === 1) { + return true; + } + + if ($support->isAnonymous()) { + return true; + } + + return false; + } + + private function getSupportMessage(array $record, Support $support, array $context): ?string + { + $message = $support->getMessage(); + $msg = $this->getInvestMsg($record, $context); + + if (!$msg || empty($msg['msg'])) { + return $message; + } + + if (!$message) { + return $msg['msg']; + } + + return \sprintf("\n***\n%s", $msg['msg']); + } + private function getPlatformTipjar(): Tipjar { $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); @@ -192,6 +250,17 @@ class CheckoutsPump implements PumpInterface return $tipjar; } + private function getInvestMsg(array $record, array $context) + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest_msg` m WHERE m.invest = :invest' + ); + + $query->execute(['invest' => $record['id']]); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + private function getChargeType(array $record): ChargeType { if (\in_array($record['method'], ['stripe_subscription'])) { From 19717b6484076d30f33e63aa33a2883a38656147 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 5 Sep 2025 20:50:49 +0200 Subject: [PATCH 155/203] Filter ProjectSupports by anonimity --- src/ApiResource/Project/SupportApiResource.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index c5286f4..c70170d 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Project; +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; @@ -72,6 +73,7 @@ class SupportApiResource */ #[Assert\NotNull()] #[Assert\Type('bool')] + #[API\ApiFilter(BooleanFilter::class)] public bool $anonymous = true; /** From 892fe559a5a2d32d1a19dded8af8e7c38d194f2d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sat, 6 Sep 2025 12:58:30 +0200 Subject: [PATCH 156/203] Rename Library\Economy to Money --- config/services.yaml | 4 ++-- src/Gateway/Wallet/WalletGateway.php | 2 +- src/Gateway/Wallet/WalletService.php | 2 +- src/Mapping/Transformer/SupportMoneyMapTransformer.php | 2 +- src/Matchfunding/Formula/MultiplicationFormula.php | 2 +- src/Matchfunding/Formula/PercentageFormula.php | 2 +- .../Currency/EuropeanCentralBankExchange.php | 4 ++-- src/{Library/Economy => Money}/Currency/ExchangeInterface.php | 2 +- src/{Library/Economy => Money}/Currency/ExchangeLocator.php | 2 +- .../Economy => Money}/Currency/FrankfurterExchange.php | 4 ++-- src/{Library/Economy => Money}/MoneyService.php | 4 ++-- src/Service/AccountingService.php | 2 +- src/Service/Project/BudgetService.php | 2 +- src/Validator/EnoughRewardChargeValidator.php | 2 +- .../Economy/Currency/EuropeanCentralBankExchangeTest.php | 2 +- tests/Library/Economy/MoneyServiceTest.php | 2 +- 16 files changed, 20 insertions(+), 20 deletions(-) rename src/{Library/Economy => Money}/Currency/EuropeanCentralBankExchange.php (98%) rename src/{Library/Economy => Money}/Currency/ExchangeInterface.php (97%) rename src/{Library/Economy => Money}/Currency/ExchangeLocator.php (98%) rename src/{Library/Economy => Money}/Currency/FrankfurterExchange.php (96%) rename src/{Library/Economy => Money}/MoneyService.php (96%) diff --git a/config/services.yaml b/config/services.yaml index f79be2c..8282741 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,7 +14,7 @@ services: _instanceof: App\Gateway\GatewayInterface: tags: ['app.gateway.gateway'] - App\Library\Economy\Currency\ExchangeInterface: + App\Money\Currency\ExchangeInterface: tags: ['app.lib.economy.currency.exchange'] App\Matchfunding\Formula\FormulaInterface: tags: ['app.matchfunding.formula'] @@ -94,7 +94,7 @@ services: $hosts: - 'peertube.plataformess.org' - App\Library\Economy\Currency\ExchangeLocator: + App\Money\Currency\ExchangeLocator: arguments: - !tagged 'app.lib.economy.currency.exchange' diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index e380239..6203497 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -10,7 +10,7 @@ use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\GatewayInterface; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index 3ef2724..21e085a 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -7,7 +7,7 @@ use App\Entity\Accounting\Transaction; use App\Entity\Money; use App\Entity\WalletFinancement; use App\Entity\WalletStatement; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; use App\Repository\WalletStatementRepository; use Brick\Money as Brick; use Doctrine\ORM\EntityManagerInterface; diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php index b5998aa..459d75f 100644 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ b/src/Mapping/Transformer/SupportMoneyMapTransformer.php @@ -4,7 +4,7 @@ namespace App\Mapping\Transformer; use App\Entity\Money; use App\Entity\Project\Support; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; class SupportMoneyMapTransformer implements PropertyTransformerInterface diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php index 105f5fa..10ccf38 100644 --- a/src/Matchfunding/Formula/MultiplicationFormula.php +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -3,7 +3,7 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; class MultiplicationFormula implements FormulaInterface { diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php index ecd1c0f..55c6446 100644 --- a/src/Matchfunding/Formula/PercentageFormula.php +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -3,7 +3,7 @@ namespace App\Matchfunding\Formula; use App\Entity\Money; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; class PercentageFormula implements FormulaInterface { diff --git a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php b/src/Money/Currency/EuropeanCentralBankExchange.php similarity index 98% rename from src/Library/Economy/Currency/EuropeanCentralBankExchange.php rename to src/Money/Currency/EuropeanCentralBankExchange.php index 976e247..ec40f11 100644 --- a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php +++ b/src/Money/Currency/EuropeanCentralBankExchange.php @@ -1,9 +1,9 @@ Date: Sat, 6 Sep 2025 15:01:03 +0200 Subject: [PATCH 157/203] Reduce deprecation notices --- config/packages/zenstruck_foundry.yaml | 2 ++ src/Entity/Project/Support.php | 5 ++--- src/EventSubscriber/AuthorizationTokenSubscriber.php | 2 +- src/Money/Currency/ExchangeLocator.php | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/packages/zenstruck_foundry.yaml b/config/packages/zenstruck_foundry.yaml index e3ed04b..71fd8fe 100644 --- a/config/packages/zenstruck_foundry.yaml +++ b/config/packages/zenstruck_foundry.yaml @@ -1,5 +1,7 @@ when@dev: &dev # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration zenstruck_foundry: + persistence: + flush_once: true when@test: *dev diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index d37c05c..6a7f414 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -12,10 +12,9 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[MapProvider(EntityMapProvider::class)] +#[ORM\Table(name: 'project_support')] +#[ORM\UniqueConstraint(fields: ['project', 'origin'])] #[ORM\Entity(repositoryClass: SupportRepository::class)] -#[ORM\Table(name: 'project_support', uniqueConstraints: [ - new ORM\UniqueConstraint(fields: ['project', 'origin']), -])] class Support { #[ORM\Id] diff --git a/src/EventSubscriber/AuthorizationTokenSubscriber.php b/src/EventSubscriber/AuthorizationTokenSubscriber.php index 1c9541f..e9a11a4 100644 --- a/src/EventSubscriber/AuthorizationTokenSubscriber.php +++ b/src/EventSubscriber/AuthorizationTokenSubscriber.php @@ -17,7 +17,7 @@ final class AuthorizationTokenSubscriber implements EventSubscriberInterface private AuthService $authService, ) {} - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::VIEW => ['onView', EventPriorities::POST_WRITE], diff --git a/src/Money/Currency/ExchangeLocator.php b/src/Money/Currency/ExchangeLocator.php index f4970c8..1a3ee12 100644 --- a/src/Money/Currency/ExchangeLocator.php +++ b/src/Money/Currency/ExchangeLocator.php @@ -24,7 +24,7 @@ class ExchangeLocator * @param ExchangeInterface $b */ usort($exchanges, function ($a, $b) { - return $a->getWeight() < $b->getWeight(); + return $a->getWeight() <=> $b->getWeight(); }); foreach ($exchanges as $exchange) { From 29988c82b79cb5e651e9ef0576399f82c6def650 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sat, 6 Sep 2025 15:01:18 +0200 Subject: [PATCH 158/203] Refactor Money to use MoneyInterface --- .../Currency/EuropeanCentralBankExchange.php | 5 ++-- src/Money/Currency/ExchangeInterface.php | 10 +++---- src/Money/Currency/FrankfurterExchange.php | 5 ++-- src/Money/Money.php | 21 ++++++++++++++ src/Money/MoneyInterface.php | 16 ++++++++++ src/Money/MoneyService.php | 29 +++++++++---------- tests/Library/Economy/MoneyServiceTest.php | 24 +++++++-------- 7 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 src/Money/Money.php create mode 100644 src/Money/MoneyInterface.php diff --git a/src/Money/Currency/EuropeanCentralBankExchange.php b/src/Money/Currency/EuropeanCentralBankExchange.php index ec40f11..e293acf 100644 --- a/src/Money/Currency/EuropeanCentralBankExchange.php +++ b/src/Money/Currency/EuropeanCentralBankExchange.php @@ -2,7 +2,8 @@ namespace App\Money\Currency; -use App\Entity\Money; +use App\Money\Money; +use App\Money\MoneyInterface; use App\Money\MoneyService; use Brick\Math\RoundingMode; use Brick\Money\CurrencyConverter; @@ -71,7 +72,7 @@ class EuropeanCentralBankExchange implements ExchangeInterface return self::WEIGHT; } - public function convert(Money $money, string $toCurrency): Money + public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface { $converted = $this->converter->convert( MoneyService::toBrick($money), diff --git a/src/Money/Currency/ExchangeInterface.php b/src/Money/Currency/ExchangeInterface.php index 5f13553..94fb469 100644 --- a/src/Money/Currency/ExchangeInterface.php +++ b/src/Money/Currency/ExchangeInterface.php @@ -2,7 +2,7 @@ namespace App\Money\Currency; -use App\Entity\Money; +use App\Money\MoneyInterface; use Brick\Money\Exception\CurrencyConversionException; interface ExchangeInterface @@ -18,14 +18,14 @@ interface ExchangeInterface public function getWeight(): int; /** - * @param Money $money The money to be converted - * @param string $toCurrency The currency to convert to + * @param MoneyInterface $money The money to be converted + * @param string $toCurrency The currency to convert to * - * @return Money The converted Money + * @return MoneyInterface The converted MoneyInterface * * @throws CurrencyConversionException If the exchange rate is not available */ - public function convert(Money $money, string $toCurrency): Money; + public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface; /** * @param string $fromCurrency The currency to convert from diff --git a/src/Money/Currency/FrankfurterExchange.php b/src/Money/Currency/FrankfurterExchange.php index d82b0fd..04fef52 100644 --- a/src/Money/Currency/FrankfurterExchange.php +++ b/src/Money/Currency/FrankfurterExchange.php @@ -2,7 +2,8 @@ namespace App\Money\Currency; -use App\Entity\Money; +use App\Money\Money; +use App\Money\MoneyInterface; use App\Money\MoneyService; use Brick\Math\RoundingMode; use Brick\Money\CurrencyConverter; @@ -49,7 +50,7 @@ class FrankfurterExchange implements ExchangeInterface $this->converter = new CurrencyConverter($this->provider); } - public function convert(Money $money, string $toCurrency): Money + public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface { $converted = $this->converter->convert( MoneyService::toBrick($money), diff --git a/src/Money/Money.php b/src/Money/Money.php new file mode 100644 index 0000000..d714474 --- /dev/null +++ b/src/Money/Money.php @@ -0,0 +1,21 @@ +amount; + } + + public function getCurrency(): string + { + return $this->currency; + } +} diff --git a/src/Money/MoneyInterface.php b/src/Money/MoneyInterface.php new file mode 100644 index 0000000..ed1f99a --- /dev/null +++ b/src/Money/MoneyInterface.php @@ -0,0 +1,16 @@ +getMinorAmount()->toInt(), @@ -20,17 +19,17 @@ class MoneyService ); } - public static function toBrick(Money $money): Brick\Money + public static function toBrick(MoneyInterface $money): BrickMoney { - return Brick\Money::ofMinor($money->amount, $money->currency); + return BrickMoney::ofMinor($money->getAmount(), $money->getCurrency()); } /** * Adds `a` to `b`. */ - public function add(Money $a, Money $b): Money + public function add(MoneyInterface $a, MoneyInterface $b): Money { - $a = $this->convert($a, $b->currency); + $a = $this->convert($a, $b->getCurrency()); $ab = self::toBrick($b)->plus($a); return self::toMoney($ab); @@ -39,9 +38,9 @@ class MoneyService /** * Substracts `a` from `b`. */ - public function substract(Money $a, Money $b): Money + public function substract(MoneyInterface $a, MoneyInterface $b): Money { - $a = $this->convert($a, $b->currency); + $a = $this->convert($a, $b->getCurrency()); $ab = self::toBrick($b)->minus($a); return self::toMoney($ab); @@ -52,9 +51,9 @@ class MoneyService * * @return bool `true` if `$money` is less than `$than` */ - public function isLess(Money $money, Money $than): bool + public function isLess(MoneyInterface $money, MoneyInterface $than): bool { - $money = $this->convert($money, $than->currency); + $money = $this->convert($money, $than->getCurrency()); return $money->isLessThan(self::toBrick($than)); } @@ -64,16 +63,16 @@ class MoneyService * * @return bool `true` if `$money` is more than or same as `$than` */ - public function isMoreOrSame(Money $money, Money $than): bool + public function isMoreOrSame(MoneyInterface $money, MoneyInterface $than): bool { - $money = $this->convert($money, $than->currency); + $money = $this->convert($money, $than->getCurrency()); return $money->isGreaterThanOrEqualTo(self::toBrick($than)); } - private function convert(Money $money, string $toCurrency): Brick\Money + private function convert(MoneyInterface $money, string $toCurrency): BrickMoney { - $fromCurrency = $money->currency; + $fromCurrency = $money->getCurrency(); if ($fromCurrency === $toCurrency) { return self::toBrick($money); } diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php index f3b04bd..8255e08 100644 --- a/tests/Library/Economy/MoneyServiceTest.php +++ b/tests/Library/Economy/MoneyServiceTest.php @@ -2,7 +2,7 @@ namespace App\Tests\Library\Economy; -use App\Entity\Money; +use App\Money\Money; use App\Money\MoneyService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -64,17 +64,17 @@ class MoneyServiceTest extends KernelTestCase $c = $this->moneyService->add($a, $b); - $this->assertEquals(100, $a->amount); - $this->assertEquals(110, $b->amount); - $this->assertEquals(210, $c->amount); + $this->assertEquals(100, $a->getAmount()); + $this->assertEquals(110, $b->getAmount()); + $this->assertEquals(210, $c->getAmount()); $d = new Money(100, 'USD'); $e = $this->moneyService->add($a, $d); - $this->assertEquals($d->amount, $a->amount); - $this->assertEquals($d->currency, $e->currency); + $this->assertEquals($d->getAmount(), $a->getAmount()); + $this->assertEquals($d->getCurrency(), $e->getCurrency()); - $this->assertNotEquals($d->amount, $e->amount); + $this->assertNotEquals($d->getAmount(), $e->getAmount()); $this->assertTrue($this->moneyService->isMoreOrSame($e, $d)); $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); } @@ -86,16 +86,16 @@ class MoneyServiceTest extends KernelTestCase $c = $this->moneyService->substract($a, $b); - $this->assertEquals(101, $a->amount); - $this->assertEquals(210, $b->amount); - $this->assertEquals(109, $c->amount); + $this->assertEquals(101, $a->getAmount()); + $this->assertEquals(210, $b->getAmount()); + $this->assertEquals(109, $c->getAmount()); $d = new Money(500, 'USD'); $e = $this->moneyService->substract($a, $d); - $this->assertEquals($d->currency, $e->currency); + $this->assertEquals($d->getCurrency(), $e->getCurrency()); - $this->assertNotEquals($d->amount, $e->amount); + $this->assertNotEquals($d->getAmount(), $e->getAmount()); $this->assertTrue($this->moneyService->isLess($e, $d)); $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); } From c223a13c8ff0d66081d516976da6d753914253ae Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sat, 6 Sep 2025 20:36:10 +0200 Subject: [PATCH 159/203] Rename Entity\Money and ApiResource\Money classes --- .../Accounting/AccountingBalance.php | 5 +- .../Accounting/AccountingBalancePoint.php | 4 +- .../Accounting/TransactionApiResource.php | 4 +- src/ApiResource/ApiMoney.php | 50 +++++++++ src/ApiResource/Gateway/ChargeApiResource.php | 4 +- .../Matchfunding/MatchStrategyApiResource.php | 4 +- src/ApiResource/Money.php | 23 ---- .../Project/BudgetItemApiResource.php | 4 +- src/ApiResource/Project/BudgetSummary.php | 10 +- src/ApiResource/Project/RewardApiResource.php | 4 +- .../Project/SupportApiResource.php | 4 +- src/Benzina/CheckoutsPump.php | 6 +- src/Benzina/ProjectsBudgetPump.php | 4 +- src/Benzina/ProjectsRewardsPump.php | 4 +- src/Entity/Accounting/Transaction.php | 2 +- src/Entity/{Money.php => EmbeddableMoney.php} | 21 +++- src/Entity/Gateway/Charge.php | 2 +- src/Entity/Matchfunding/MatchStrategy.php | 2 +- src/Entity/Project/BudgetItem.php | 2 +- src/Entity/Project/Reward.php | 2 +- src/Entity/{ => Wallet}/WalletFinancement.php | 13 ++- src/Entity/{ => Wallet}/WalletStatement.php | 13 ++- src/Gateway/Wallet/WalletGateway.php | 2 +- src/Gateway/Wallet/WalletService.php | 30 +++-- .../Transformer/BudgetMapTransformer.php | 17 +-- .../SupportMoneyMapTransformer.php | 5 +- src/Matchfunding/Formula/FormulaInterface.php | 11 +- .../Formula/MultiplicationFormula.php | 8 +- .../Formula/PercentageFormula.php | 9 +- .../WalletFinancementRepository.php | 4 +- .../WalletStatementRepository.php | 4 +- src/Service/AccountingService.php | 8 +- src/Service/Project/BudgetService.php | 2 +- tests/Gateway/Wallet/WalletServiceTest.php | 104 +++++++++--------- tests/Service/AccountingServiceTest.php | 4 +- 35 files changed, 226 insertions(+), 169 deletions(-) create mode 100644 src/ApiResource/ApiMoney.php delete mode 100644 src/ApiResource/Money.php rename src/Entity/{Money.php => EmbeddableMoney.php} (61%) rename src/Entity/{ => Wallet}/WalletFinancement.php (82%) rename src/Entity/{ => Wallet}/WalletStatement.php (93%) rename src/Repository/{ => Wallet}/WalletFinancementRepository.php (94%) rename src/Repository/{ => Wallet}/WalletStatementRepository.php (97%) diff --git a/src/ApiResource/Accounting/AccountingBalance.php b/src/ApiResource/Accounting/AccountingBalance.php index 4bb9c71..d8a7b57 100644 --- a/src/ApiResource/Accounting/AccountingBalance.php +++ b/src/ApiResource/Accounting/AccountingBalance.php @@ -3,8 +3,7 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Metadata as API; -use App\Entity\Accounting\Accounting; -use App\Entity\Money; +use App\ApiResource\ApiMoney; use App\State\Accounting\AccountingBalanceStateProvider; /** @@ -35,5 +34,5 @@ class AccountingBalance * The money currently held by the Accounting. */ #[API\ApiProperty(security: 'is_granted("ACCOUNTING_VIEW", object.accounting)')] - public Money $balance; + public ApiMoney $balance; } diff --git a/src/ApiResource/Accounting/AccountingBalancePoint.php b/src/ApiResource/Accounting/AccountingBalancePoint.php index 6ca16fc..80c3c11 100644 --- a/src/ApiResource/Accounting/AccountingBalancePoint.php +++ b/src/ApiResource/Accounting/AccountingBalancePoint.php @@ -4,7 +4,7 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Metadata as API; use ApiPlatform\Metadata\QueryParameter; -use App\Entity\Money; +use App\ApiResource\ApiMoney; use App\State\Accounting\AccountingBalancePointStateProvider; use Symfony\Component\Validator\Constraints as Assert; @@ -57,7 +57,7 @@ class AccountingBalancePoint /** * Resulting balance for items in this point. */ - public Money $balance; + public ApiMoney $balance; /** * The number of items aggregated in this point. diff --git a/src/ApiResource/Accounting/TransactionApiResource.php b/src/ApiResource/Accounting/TransactionApiResource.php index 85e13c6..06ba51a 100644 --- a/src/ApiResource/Accounting/TransactionApiResource.php +++ b/src/ApiResource/Accounting/TransactionApiResource.php @@ -5,8 +5,8 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; +use App\ApiResource\ApiMoney; use App\Entity\Accounting\Transaction; -use App\Entity\Money; use App\State\ApiResourceStateProvider; /** @@ -33,7 +33,7 @@ class TransactionApiResource /** * The monetary value received at target and issued at origin. */ - public Money $money; + public ApiMoney $money; /** * The Accounting from which the Transaction comes from. diff --git a/src/ApiResource/ApiMoney.php b/src/ApiResource/ApiMoney.php new file mode 100644 index 0000000..9f2e3fd --- /dev/null +++ b/src/ApiResource/ApiMoney.php @@ -0,0 +1,50 @@ +amount = $amount; + $this->currency = $currency; + } + + public static function of(MoneyInterface $moneyInterface): self + { + return new self( + $moneyInterface->getAmount(), + $moneyInterface->getCurrency() + ); + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } +} diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index c7480f6..ef8db2c 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -6,9 +6,9 @@ use ApiPlatform\Doctrine\Orm\Filter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; +use App\ApiResource\ApiMoney; use App\Dto\Gateway\ChargeUpdationDto; use App\Entity\Gateway\Charge; -use App\Entity\Money; use App\Filter\GatewayFilter; use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; @@ -81,7 +81,7 @@ class ChargeApiResource #[Assert\NotBlank()] #[API\ApiFilter(Filter\RangeFilter::class, properties: ['money.amount'])] #[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.currency' => 'exact'])] - public Money $money; + public ApiMoney $money; /** * The status of the charge item with the Gateway. diff --git a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php index c13485a..a58c382 100644 --- a/src/ApiResource/Matchfunding/MatchStrategyApiResource.php +++ b/src/ApiResource/Matchfunding/MatchStrategyApiResource.php @@ -6,7 +6,7 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; -use App\ApiResource\Money; +use App\ApiResource\ApiMoney; use App\Entity\Matchfunding\MatchAgainst; use App\Entity\Matchfunding\MatchStrategy; use App\Mapping\Transformer\MatchFormulaMapTransformer; @@ -81,7 +81,7 @@ class MatchStrategyApiResource */ #[Assert\NotBlank()] #[Assert\Valid()] - public Money $limit; + public ApiMoney $limit; /** * The `x` factor used to calculate the resulting match of funds with the MatchFormula. diff --git a/src/ApiResource/Money.php b/src/ApiResource/Money.php deleted file mode 100644 index 9fba1f0..0000000 --- a/src/ApiResource/Money.php +++ /dev/null @@ -1,23 +0,0 @@ -setType($this->getCostType($record)); $budgetItem->setTitle($record['cost']); $budgetItem->setDescription($record['description'] ?? $record['cost']); - $budgetItem->setMoney(new Money($record['amount'] * 100, 'EUR')); + $budgetItem->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); $budgetItem->setDeadline($this->getDeadline($record)); $localizations = $this->getCostLocalizations($budgetItem, $context); diff --git a/src/Benzina/ProjectsRewardsPump.php b/src/Benzina/ProjectsRewardsPump.php index 8c53a22..04795ac 100644 --- a/src/Benzina/ProjectsRewardsPump.php +++ b/src/Benzina/ProjectsRewardsPump.php @@ -2,7 +2,7 @@ namespace App\Benzina; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\Reward; @@ -73,7 +73,7 @@ class ProjectsRewardsPump implements PumpInterface $reward->setMigratedId($record['id']); $reward->setTitle($record['reward']); $reward->setDescription($record['description'] ?? $record['reward']); - $reward->setMoney(new Money($record['amount'] * 100, 'EUR')); + $reward->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); $reward->setHasUnits($record['units'] > 0); $reward->setUnitsTotal($record['units'] ?? 0); $reward->setUnitsAvailable($record['units'] ?? 0); diff --git a/src/Entity/Accounting/Transaction.php b/src/Entity/Accounting/Transaction.php index 2de26fc..c630345 100644 --- a/src/Entity/Accounting/Transaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -2,7 +2,7 @@ namespace App\Entity\Accounting; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Trait\TimestampedCreationEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Accounting\TransactionRepository; diff --git a/src/Entity/Money.php b/src/Entity/EmbeddableMoney.php similarity index 61% rename from src/Entity/Money.php rename to src/Entity/EmbeddableMoney.php index c3625d5..9f79eae 100644 --- a/src/Entity/Money.php +++ b/src/Entity/EmbeddableMoney.php @@ -2,6 +2,7 @@ namespace App\Entity; +use App\Money\MoneyInterface; use Doctrine\ORM\Mapping as ORM; /** @@ -11,7 +12,7 @@ use Doctrine\ORM\Mapping as ORM; * @see \Brick\Money */ #[ORM\Embeddable] -class Money +class EmbeddableMoney implements MoneyInterface { /** * An amount of currency.\ @@ -33,4 +34,22 @@ class Money $this->amount = $amount; $this->currency = $currency; } + + public static function of(MoneyInterface $moneyInterface): self + { + return new self( + $moneyInterface->getAmount(), + $moneyInterface->getCurrency() + ); + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } } diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index 8bf56f4..cd52fc1 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -4,7 +4,7 @@ namespace App\Entity\Gateway; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\ChargeStatus; diff --git a/src/Entity/Matchfunding/MatchStrategy.php b/src/Entity/Matchfunding/MatchStrategy.php index 8f9b002..7f45ba1 100644 --- a/src/Entity/Matchfunding/MatchStrategy.php +++ b/src/Entity/Matchfunding/MatchStrategy.php @@ -2,7 +2,7 @@ namespace App\Entity\Matchfunding; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; use App\Mapping\Provider\EntityMapProvider; use App\Matchfunding\Formula\MultiplicationFormula; use App\Repository\Matchfunding\MatchStrategyRepository; diff --git a/src/Entity/Project/BudgetItem.php b/src/Entity/Project/BudgetItem.php index 4933eb5..0d752f8 100644 --- a/src/Entity/Project/BudgetItem.php +++ b/src/Entity/Project/BudgetItem.php @@ -2,8 +2,8 @@ namespace App\Entity\Project; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Interface\LocalizedEntityInterface; -use App\Entity\Money; use App\Entity\Trait\LocalizedEntityTrait; use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index dc3f60d..6ea2a3b 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -2,8 +2,8 @@ namespace App\Entity\Project; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Interface\LocalizedEntityInterface; -use App\Entity\Money; use App\Entity\Trait\LocalizedEntityTrait; use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; diff --git a/src/Entity/WalletFinancement.php b/src/Entity/Wallet/WalletFinancement.php similarity index 82% rename from src/Entity/WalletFinancement.php rename to src/Entity/Wallet/WalletFinancement.php index 34f7ddc..de8f454 100644 --- a/src/Entity/WalletFinancement.php +++ b/src/Entity/Wallet/WalletFinancement.php @@ -1,8 +1,9 @@ id; } - public function getMoney(): ?Money + public function getMoney(): ?EmbeddableMoney { return $this->money; } - public function setMoney(Money $money): static + public function setMoney(EmbeddableMoney $money): static { $this->money = $money; diff --git a/src/Entity/WalletStatement.php b/src/Entity/Wallet/WalletStatement.php similarity index 93% rename from src/Entity/WalletStatement.php rename to src/Entity/Wallet/WalletStatement.php index be15c70..1a91524 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/Wallet/WalletStatement.php @@ -1,10 +1,11 @@ balance; } - public function setBalance(Money $balance): static + public function setBalance(EmbeddableMoney $balance): static { $this->balance = $balance; diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index 6203497..d0bcbde 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -5,11 +5,11 @@ namespace App\Gateway\Wallet; use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; -use App\Entity\Money; use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\GatewayInterface; +use App\Money\Money; use App\Money\MoneyService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index 21e085a..66f14a2 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -4,11 +4,12 @@ namespace App\Gateway\Wallet; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; -use App\Entity\Money; -use App\Entity\WalletFinancement; -use App\Entity\WalletStatement; +use App\Entity\EmbeddableMoney; +use App\Entity\Wallet\WalletFinancement; +use App\Entity\Wallet\WalletStatement; +use App\Money\Money; use App\Money\MoneyService; -use App\Repository\WalletStatementRepository; +use App\Repository\Wallet\WalletStatementRepository; use Brick\Money as Brick; use Doctrine\ORM\EntityManagerInterface; @@ -39,7 +40,10 @@ class WalletService $total = $total->plus($balance); } - return $this->money->toMoney($total); + return new Money( + $total->getMinorAmount()->toInt(), + $total->getCurrency()->getCurrencyCode() + ); } /** @@ -77,7 +81,7 @@ class WalletService $origin = $transaction->getOrigin(); $spendGoal = $transaction->getMoney(); - $spentTotal = new Money(0, $spendGoal->currency); + $spentTotal = new Money(0, $spendGoal->getCurrency()); $outgoing = new WalletStatement(); $outgoing->setTransaction($transaction); @@ -85,7 +89,7 @@ class WalletService $statements = $this->getStatements($origin); foreach ($statements as $statement) { - if ($spendGoal->amount === 0) { + if ($spendGoal->getAmount() === 0) { break; } @@ -94,9 +98,9 @@ class WalletService } $incoming = $statement; - $balance = $incoming->getBalance(); - if ($balance->amount === 0) { + $balance = $incoming->getBalance(); + if ($balance->getAmount() === 0) { continue; } @@ -107,12 +111,14 @@ class WalletService } $financement = new WalletFinancement(); - $financement->setMoney($balanceSpent); + $financement->setMoney(EmbeddableMoney::of($balanceSpent)); - $incoming->setBalance($this->money->substract($balanceSpent, $balance)); + $substracted = $this->money->substract($balanceSpent, $balance); + $incoming->setBalance(EmbeddableMoney::of($substracted)); $incoming->addFinancesTo($financement); - $outgoing->setBalance($this->money->add($balanceSpent, $spentTotal)); + $added = $this->money->add($balanceSpent, $spentTotal); + $outgoing->setBalance(EmbeddableMoney::of($added)); $outgoing->addFinancedBy($financement); $this->entityManager->persist($incoming); diff --git a/src/Mapping/Transformer/BudgetMapTransformer.php b/src/Mapping/Transformer/BudgetMapTransformer.php index c9af004..c9bdd15 100644 --- a/src/Mapping/Transformer/BudgetMapTransformer.php +++ b/src/Mapping/Transformer/BudgetMapTransformer.php @@ -2,6 +2,7 @@ namespace App\Mapping\Transformer; +use App\ApiResource\ApiMoney; use App\ApiResource\Project\Budget; use App\ApiResource\Project\BudgetSummary; use App\Entity\Project\BudgetItem; @@ -45,25 +46,25 @@ class BudgetMapTransformer implements PropertyTransformerInterface $summary = new BudgetSummary(); - $summary->money = $this->budgetService->calcBudget( + $summary->money = ApiMoney::of($this->budgetService->calcBudget( $items, $currency - )[$deadline]; + )[$deadline]); - $summary->task = $this->budgetService->calcBudget( + $summary->task = ApiMoney::of($this->budgetService->calcBudget( $this->filterItemsByType($items, BudgetItemType::Task), $currency - )[$deadline]; + )[$deadline]); - $summary->material = $this->budgetService->calcBudget( + $summary->material = ApiMoney::of($this->budgetService->calcBudget( $this->filterItemsByType($items, BudgetItemType::Material), $currency - )[$deadline]; + )[$deadline]); - $summary->infra = $this->budgetService->calcBudget( + $summary->infra = ApiMoney::of($this->budgetService->calcBudget( $this->filterItemsByType($items, BudgetItemType::Infrastructure), $currency - )[$deadline]; + )[$deadline]); return $summary; } diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php index 459d75f..a6cccd2 100644 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ b/src/Mapping/Transformer/SupportMoneyMapTransformer.php @@ -2,8 +2,9 @@ namespace App\Mapping\Transformer; -use App\Entity\Money; +use App\ApiResource\ApiMoney; use App\Entity\Project\Support; +use App\Money\Money; use App\Money\MoneyService; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; @@ -25,6 +26,6 @@ class SupportMoneyMapTransformer implements PropertyTransformerInterface $money = $this->moneyService->add($transaction->getMoney(), $money); } - return $money; + return ApiMoney::of($money); } } diff --git a/src/Matchfunding/Formula/FormulaInterface.php b/src/Matchfunding/Formula/FormulaInterface.php index 461ed26..499c83a 100644 --- a/src/Matchfunding/Formula/FormulaInterface.php +++ b/src/Matchfunding/Formula/FormulaInterface.php @@ -2,7 +2,8 @@ namespace App\Matchfunding\Formula; -use App\Entity\Money; +use App\Money\Money; +use App\Money\MoneyInterface; interface FormulaInterface { @@ -19,11 +20,11 @@ interface FormulaInterface /** * Process an input Money and obtain a delta Money, which is the Money to be given by the Matcher. * - * @param float $factor A matching factor for the formula to use - * @param Money $money The input Money that is to be matched - * @param Money $limit An upper bound for the matching formula, the delta won't be higher than this + * @param float $factor A matching factor for the formula to use + * @param MoneyInterface $money The input Money that is to be matched + * @param MoneyInterface $limit An upper bound for the matching formula, the delta won't be higher than this * * @return Money The output Money delta */ - public function match(float $factor, Money $money, Money $limit): Money; + public function match(float $factor, MoneyInterface $money, MoneyInterface $limit): Money; } diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php index 10ccf38..a989a8a 100644 --- a/src/Matchfunding/Formula/MultiplicationFormula.php +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -2,7 +2,8 @@ namespace App\Matchfunding\Formula; -use App\Entity\Money; +use App\Money\Money; +use App\Money\MoneyInterface; use App\Money\MoneyService; class MultiplicationFormula implements FormulaInterface @@ -17,12 +18,11 @@ class MultiplicationFormula implements FormulaInterface return 'min(factor * money, limit)'; } - public function match(float $factor, Money $money, Money $limit): Money + public function match(float $factor, MoneyInterface $money, MoneyInterface $limit): Money { $delta = \Brick\Money\Money::min( MoneyService::toBrick($limit), - MoneyService::toBrick($money) - ->multipliedBy($factor) + MoneyService::toBrick($money)->multipliedBy($factor) ); return MoneyService::toMoney($delta); diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php index 55c6446..50328a5 100644 --- a/src/Matchfunding/Formula/PercentageFormula.php +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -2,7 +2,8 @@ namespace App\Matchfunding\Formula; -use App\Entity\Money; +use App\Money\Money; +use App\Money\MoneyInterface; use App\Money\MoneyService; class PercentageFormula implements FormulaInterface @@ -17,13 +18,11 @@ class PercentageFormula implements FormulaInterface return 'min(factor * money / 100, limit)'; } - public function match(float $factor, Money $money, Money $limit): Money + public function match(float $factor, MoneyInterface $money, MoneyInterface $limit): Money { $delta = \Brick\Money\Money::min( MoneyService::toBrick($limit), - MoneyService::toBrick($money) - ->multipliedBy($factor) - ->dividedBy(100) + MoneyService::toBrick($money)->multipliedBy($factor)->dividedBy(100) ); return MoneyService::toMoney($delta); diff --git a/src/Repository/WalletFinancementRepository.php b/src/Repository/Wallet/WalletFinancementRepository.php similarity index 94% rename from src/Repository/WalletFinancementRepository.php rename to src/Repository/Wallet/WalletFinancementRepository.php index 0704d8c..aad9e74 100644 --- a/src/Repository/WalletFinancementRepository.php +++ b/src/Repository/Wallet/WalletFinancementRepository.php @@ -1,8 +1,8 @@ start = $lowerBound; $point->end = $upperBound; - $point->balance = $balance; + $point->balance = ApiMoney::of($balance); $point->length = count($transactions); return $point; diff --git a/src/Service/Project/BudgetService.php b/src/Service/Project/BudgetService.php index a8ad0ac..fb9f24f 100644 --- a/src/Service/Project/BudgetService.php +++ b/src/Service/Project/BudgetService.php @@ -2,9 +2,9 @@ namespace App\Service\Project; -use App\Entity\Money; use App\Entity\Project\BudgetItem; use App\Entity\Project\ProjectDeadline; +use App\Money\Money; use App\Money\MoneyService; class BudgetService diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index d7c45c7..65ba9af 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -3,7 +3,7 @@ namespace App\Tests\Gateway\Wallet; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Tipjar; use App\Entity\User\User; use App\Gateway\Wallet\StatementDirection; @@ -62,19 +62,19 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(0, $balance->amount); - $this->assertEquals($user->getCurrency(), $balance->currency); + $this->assertEquals(0, $balance->getAmount()); + $this->assertEquals($user->getCurrency(), $balance->getCurrency()); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); $income = $this->walletService->save($incoming); - $this->assertEquals(100, $income->getBalance()->amount); + $this->assertEquals(100, $income->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $income->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $income->getFinancesTo()); $this->assertCount(0, $income->getFinancedBy()); @@ -83,17 +83,17 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(100, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(100, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(1, $statements); $statement = $statements[0]; - $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(100, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } @@ -109,11 +109,11 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(0, $balance->amount); - $this->assertEquals($user->getCurrency(), $balance->currency); + $this->assertEquals(0, $balance->getAmount()); + $this->assertEquals($user->getCurrency(), $balance->getCurrency()); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -122,17 +122,17 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(100, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(100, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(1, $statements); $statement = $statements[0]; - $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(100, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } @@ -143,7 +143,7 @@ class WalletServiceTest extends KernelTestCase $user = $this->getUser()->getAccounting(); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -151,7 +151,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(20, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(20, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -162,29 +162,29 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(80, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(80, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(2, $statements); $statement = $statements[0]; - $this->assertEquals(80, $statement->getBalance()->amount); + $this->assertEquals(80, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(20, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(30, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(30, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -195,43 +195,43 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(50, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(50, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(3, $statements); $statement = $statements[0]; - $this->assertEquals(50, $statement->getBalance()->amount); + $this->assertEquals(50, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(2, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(20, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $financement = $statements[0]->getFinancesTo()[0]; - $this->assertEquals(20, $financement->getMoney()->amount); - $this->assertEquals('EUR', $financement->getMoney()->currency); + $this->assertEquals(20, $financement->getMoney()->getAmount()); + $this->assertEquals('EUR', $financement->getMoney()->getCurrency()); $this->assertSame($financement, $statement->getFinancedBy()[0]); $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); $statement = $statements[2]; - $this->assertEquals(30, $statement->getBalance()->amount); + $this->assertEquals(30, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $financement = $statements[0]->getFinancesTo()[1]; - $this->assertEquals(30, $financement->getMoney()->amount); - $this->assertEquals('EUR', $financement->getMoney()->currency); + $this->assertEquals(30, $financement->getMoney()->getAmount()); + $this->assertEquals('EUR', $financement->getMoney()->getCurrency()); $this->assertSame($financement, $statement->getFinancedBy()[0]); $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); } @@ -242,7 +242,7 @@ class WalletServiceTest extends KernelTestCase $user = $this->getUser()->getAccounting(); $incoming = new Transaction(); - $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(10, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -250,7 +250,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $incoming = new Transaction(); - $incoming->setMoney(new Money(11, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(11, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -258,7 +258,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(12, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(12, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -268,7 +268,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $incoming = new Transaction(); - $incoming->setMoney(new Money(13, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(13, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -277,38 +277,38 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(22, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(22, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(4, $statements); $statement = $statements[0]; - $this->assertEquals(0, $statement->getBalance()->amount); + $this->assertEquals(0, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(9, $statement->getBalance()->amount); + $this->assertEquals(9, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[2]; - $this->assertEquals(12, $statement->getBalance()->amount); + $this->assertEquals(12, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(2, $statement->getFinancedBy()); $statement = $statements[3]; - $this->assertEquals(13, $statement->getBalance()->amount); + $this->assertEquals(13, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } diff --git a/tests/Service/AccountingServiceTest.php b/tests/Service/AccountingServiceTest.php index 097952b..8585a54 100644 --- a/tests/Service/AccountingServiceTest.php +++ b/tests/Service/AccountingServiceTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Service; use App\ApiResource\Accounting\AccountingBalancePoint; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Tipjar; use App\Service\AccountingService; use Doctrine\ORM\EntityManagerInterface; @@ -46,7 +46,7 @@ class AccountingServiceTest extends KernelTestCase $target = $i % 2 ? $tipjars[\random_int(5, 9)] : $tipjars[\random_int(0, 5)]; $trx = new Transaction(); - $trx->setMoney(new Money(random_int(100, 999), 'EUR')); + $trx->setMoney(new EmbeddableMoney(random_int(100, 999), 'EUR')); $trx->setOrigin($origin->getAccounting()); $trx->setTarget($target->getAccounting()); From b5bb718a0a5cf94fb79ea6fa773823a6e282d17e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sat, 6 Sep 2025 21:09:38 +0200 Subject: [PATCH 160/203] Init totalization strategy --- config/services.yaml | 10 ++++- src/Money/Totalization/TotalizedMoney.php | 32 ++++++++++++++ src/Money/Totalization/TotalizerInterface.php | 16 +++++++ src/Money/Totalization/TotalizerLocator.php | 43 +++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/Money/Totalization/TotalizedMoney.php create mode 100644 src/Money/Totalization/TotalizerInterface.php create mode 100644 src/Money/Totalization/TotalizerLocator.php diff --git a/config/services.yaml b/config/services.yaml index 8282741..2269883 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -15,7 +15,9 @@ services: App\Gateway\GatewayInterface: tags: ['app.gateway.gateway'] App\Money\Currency\ExchangeInterface: - tags: ['app.lib.economy.currency.exchange'] + tags: ['app.money.currency.exchange'] + App\Money\Totalization\TotalizerInterface: + tags: ['app.money.totalization.totalizer'] App\Matchfunding\Formula\FormulaInterface: tags: ['app.matchfunding.formula'] App\Matchfunding\Rule\RuleInterface: @@ -96,7 +98,11 @@ services: App\Money\Currency\ExchangeLocator: arguments: - - !tagged 'app.lib.economy.currency.exchange' + - !tagged 'app.money.currency.exchange' + + App\Money\Totalization\TotalizerLocator: + arguments: + - !tagged 'app.money.totalization.totalizer' App\Mapping\AutoMapper: arguments: diff --git a/src/Money/Totalization/TotalizedMoney.php b/src/Money/Totalization/TotalizedMoney.php new file mode 100644 index 0000000..c1f46e8 --- /dev/null +++ b/src/Money/Totalization/TotalizedMoney.php @@ -0,0 +1,32 @@ +amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + /** + * @return int the total number of items that were totalized to produce the Money + */ + public function getLength(): int + { + return $this->length; + } +} diff --git a/src/Money/Totalization/TotalizerInterface.php b/src/Money/Totalization/TotalizerInterface.php new file mode 100644 index 0000000..fe2240a --- /dev/null +++ b/src/Money/Totalization/TotalizerInterface.php @@ -0,0 +1,16 @@ + */ + private array $totalizers; + + public function __construct( + iterable $interfaces, + ) { + /** @var TotalizerInterface[] */ + $totalizers = \iterator_to_array($interfaces); + + foreach ($totalizers as $totalizer) { + $resource = $totalizer::getSupportedResource(); + + if (\array_key_exists($resource, $this->totalizers)) { + throw new \Exception(\sprintf( + "Duplicated TotalizerInterface. The resource '%s' is already supported by %s", + $resource, + $totalizers[$resource]::class + )); + } + + $this->totalizers[$resource] = $totalizer; + } + } + + public function get(string $resource): ?TotalizerInterface + { + return $this->totalizers[$resource] ?? null; + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->totalizers; + } +} From 9f790bfc578e2b77c60905f17b4627ef880033c8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 03:46:45 +0200 Subject: [PATCH 161/203] Add Accounting default currency via env var --- .env | 5 +++- config/services.yaml | 4 +++ src/Entity/Accounting/Accounting.php | 9 ------- .../AccountingDefaultsListener.php | 27 +++++++++++++++++++ src/Money/MoneyService.php | 6 +++++ 5 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/EventListener/AccountingDefaultsListener.php diff --git a/.env b/.env index fbd996b..ce2bdc4 100644 --- a/.env +++ b/.env @@ -35,7 +35,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### ###> doctrineencryptbundle/doctrine-encrypt-bundle ### -DOCTRINE_ENCRYPT_KEY="" CRYPTO_SECRET="" ###< doctrineencryptbundle/doctrine-encrypt-bundle ### @@ -52,6 +51,10 @@ PAYPAL_CLIENT_SECRET="" PAYPAL_WEBHOOK_ID="WEBHOOK_ID" ###< App\Gateway\Paypal\PaypalGateway ### +###> App\Money\MoneyService ### +CURRENCY_DEFAULT="EUR" +###< App\Money\MoneyService ### + ###> symfony/lock ### # Choose one of the stores below # postgresql+advisory://db_user:db_password@localhost/db_name diff --git a/config/services.yaml b/config/services.yaml index 2269883..71ff37a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -96,6 +96,10 @@ services: $hosts: - 'peertube.plataformess.org' + App\Money\MoneyService: + arguments: + - '%env(CURRENCY_DEFAULT)%' + App\Money\Currency\ExchangeLocator: arguments: - !tagged 'app.money.currency.exchange' diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php index fbb4480..ac72826 100644 --- a/src/Entity/Accounting/Accounting.php +++ b/src/Entity/Accounting/Accounting.php @@ -64,15 +64,6 @@ class Accounting return $accounting; } - public function __construct() - { - /* - * TO-DO: This property must be loaded from App's configuration, - * ideally a configuration that can be updated via a frontend, not env var only - */ - $this->currency = 'EUR'; - } - public function getId(): ?int { return $this->id; diff --git a/src/EventListener/AccountingDefaultsListener.php b/src/EventListener/AccountingDefaultsListener.php new file mode 100644 index 0000000..cfbec9a --- /dev/null +++ b/src/EventListener/AccountingDefaultsListener.php @@ -0,0 +1,27 @@ +getCurrency() === null) { + $accounting->setCurrency($this->moneyService->getDefaultCurrency()); + } + } +} diff --git a/src/Money/MoneyService.php b/src/Money/MoneyService.php index 600dd9e..1da8e7d 100644 --- a/src/Money/MoneyService.php +++ b/src/Money/MoneyService.php @@ -8,9 +8,15 @@ use Brick\Money\Money as BrickMoney; class MoneyService { public function __construct( + private string $defaultCurrency, private ExchangeLocator $exchangeLocator, ) {} + public function getDefaultCurrency(): string + { + return $this->defaultCurrency; + } + public static function toMoney(BrickMoney $brick): Money { return new Money( From 408281f95e52ec27ba0458c724572a7847d548d7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 03:54:03 +0200 Subject: [PATCH 162/203] Update namespacing of Money\Currency to Money\Conversion --- config/services.yaml | 8 ++++---- .../Exchange}/EuropeanCentralBankExchange.php | 3 ++- .../Exchange}/FrankfurterExchange.php | 3 ++- src/Money/{Currency => Conversion}/ExchangeInterface.php | 2 +- src/Money/{Currency => Conversion}/ExchangeLocator.php | 2 +- src/Money/MoneyService.php | 2 +- .../Conversion}/EuropeanCentralBankExchangeTest.php | 4 ++-- tests/{Library/Economy => Money}/MoneyServiceTest.php | 2 +- 8 files changed, 14 insertions(+), 12 deletions(-) rename src/Money/{Currency => Conversion/Exchange}/EuropeanCentralBankExchange.php (98%) rename src/Money/{Currency => Conversion/Exchange}/FrankfurterExchange.php (96%) rename src/Money/{Currency => Conversion}/ExchangeInterface.php (98%) rename src/Money/{Currency => Conversion}/ExchangeLocator.php (98%) rename tests/{Library/Economy/Currency => Money/Conversion}/EuropeanCentralBankExchangeTest.php (90%) rename tests/{Library/Economy => Money}/MoneyServiceTest.php (98%) diff --git a/config/services.yaml b/config/services.yaml index 71ff37a..0587b3d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,8 +14,8 @@ services: _instanceof: App\Gateway\GatewayInterface: tags: ['app.gateway.gateway'] - App\Money\Currency\ExchangeInterface: - tags: ['app.money.currency.exchange'] + App\Money\Conversion\ExchangeInterface: + tags: ['app.money.conversion.exchange'] App\Money\Totalization\TotalizerInterface: tags: ['app.money.totalization.totalizer'] App\Matchfunding\Formula\FormulaInterface: @@ -100,9 +100,9 @@ services: arguments: - '%env(CURRENCY_DEFAULT)%' - App\Money\Currency\ExchangeLocator: + App\Money\Conversion\ExchangeLocator: arguments: - - !tagged 'app.money.currency.exchange' + - !tagged 'app.money.conversion.exchange' App\Money\Totalization\TotalizerLocator: arguments: diff --git a/src/Money/Currency/EuropeanCentralBankExchange.php b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php similarity index 98% rename from src/Money/Currency/EuropeanCentralBankExchange.php rename to src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php index e293acf..5ab7342 100644 --- a/src/Money/Currency/EuropeanCentralBankExchange.php +++ b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php @@ -1,7 +1,8 @@ Date: Sun, 7 Sep 2025 04:57:33 +0200 Subject: [PATCH 163/203] Update ExchangeInterface to leverage more from Brick converter interface --- .../Exchange/EuropeanCentralBankExchange.php | 13 +++++++++---- .../Conversion/Exchange/FrankfurterExchange.php | 13 +++++++++---- src/Money/Conversion/ExchangeInterface.php | 9 ++++++++- .../Conversion/EuropeanCentralBankExchangeTest.php | 12 ++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php index 5ab7342..2b1cf84 100644 --- a/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php +++ b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php @@ -7,6 +7,7 @@ use App\Money\Money; use App\Money\MoneyInterface; use App\Money\MoneyService; use Brick\Math\RoundingMode; +use Brick\Money\Context; use Brick\Money\CurrencyConverter; use Brick\Money\ExchangeRateProvider; use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider; @@ -73,13 +74,17 @@ class EuropeanCentralBankExchange implements ExchangeInterface return self::WEIGHT; } - public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface - { + public function convert( + MoneyInterface $money, + string $toCurrency, + ?Context $context = null, + RoundingMode $roundingMode = RoundingMode::UP, + ): MoneyInterface { $converted = $this->converter->convert( MoneyService::toBrick($money), $toCurrency, - null, - RoundingMode::HALF_EVEN + $context, + $roundingMode ); return new Money( diff --git a/src/Money/Conversion/Exchange/FrankfurterExchange.php b/src/Money/Conversion/Exchange/FrankfurterExchange.php index 6069ec5..93e59bd 100644 --- a/src/Money/Conversion/Exchange/FrankfurterExchange.php +++ b/src/Money/Conversion/Exchange/FrankfurterExchange.php @@ -7,6 +7,7 @@ use App\Money\Money; use App\Money\MoneyInterface; use App\Money\MoneyService; use Brick\Math\RoundingMode; +use Brick\Money\Context; use Brick\Money\CurrencyConverter; use Brick\Money\ExchangeRateProvider; use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider; @@ -51,13 +52,17 @@ class FrankfurterExchange implements ExchangeInterface $this->converter = new CurrencyConverter($this->provider); } - public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface - { + public function convert( + MoneyInterface $money, + string $toCurrency, + ?Context $context = null, + RoundingMode $roundingMode = RoundingMode::UP, + ): MoneyInterface { $converted = $this->converter->convert( MoneyService::toBrick($money), $toCurrency, - null, - RoundingMode::HALF_EVEN + $context, + $roundingMode ); return new Money( diff --git a/src/Money/Conversion/ExchangeInterface.php b/src/Money/Conversion/ExchangeInterface.php index 63572b4..781b320 100644 --- a/src/Money/Conversion/ExchangeInterface.php +++ b/src/Money/Conversion/ExchangeInterface.php @@ -3,6 +3,8 @@ namespace App\Money\Conversion; use App\Money\MoneyInterface; +use Brick\Math\RoundingMode; +use Brick\Money\Context; use Brick\Money\Exception\CurrencyConversionException; interface ExchangeInterface @@ -25,7 +27,12 @@ interface ExchangeInterface * * @throws CurrencyConversionException If the exchange rate is not available */ - public function convert(MoneyInterface $money, string $toCurrency): MoneyInterface; + public function convert( + MoneyInterface $money, + string $toCurrency, + ?Context $context = null, + RoundingMode $roundingMode = RoundingMode::UP, + ): MoneyInterface; /** * @param string $fromCurrency The currency to convert from diff --git a/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php b/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php index 38b1358..dae5bdd 100644 --- a/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php +++ b/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php @@ -3,6 +3,8 @@ namespace App\Tests\Money\Conversion; use App\Money\Conversion\Exchange\EuropeanCentralBankExchange; +use App\Money\Money; +use Brick\Money\Context\CustomContext; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -32,4 +34,14 @@ class EuropeanCentralBankExchangeTest extends TestCase $this->assertArrayHasKey('Cube', $exchangeData); $this->assertArrayHasKey('@attributes', $exchangeData); } + + public function testConvertsWholeEuros() + { + $exchange = new EuropeanCentralBankExchange(); + $context = new CustomContext(scale: 0, step: 1); + + $this->assertEquals(100, $exchange->convert(new Money(100, 'JPY'), 'EUR', $context)->getAmount()); + $this->assertEquals(100, $exchange->convert(new Money(150, 'JPY'), 'EUR', $context)->getAmount()); + $this->assertEquals(100, $exchange->convert(new Money(700, 'CNY'), 'EUR', $context)->getAmount()); + } } From cc581409dd62ccc3e677a51c1ea205d894a240b8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 15:36:46 +0200 Subject: [PATCH 164/203] Include conversion metadata in money --- src/ApiResource/ApiMoney.php | 20 +++- src/Entity/EmbeddableMoney.php | 16 ++- src/Money/Conversion/Conversion.php | 137 ++++++++++++++++++++++ src/Money/Money.php | 8 ++ src/Money/MoneyInterface.php | 7 ++ src/Money/Totalization/TotalizedMoney.php | 6 + 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/Money/Conversion/Conversion.php diff --git a/src/ApiResource/ApiMoney.php b/src/ApiResource/ApiMoney.php index 9f2e3fd..c22e034 100644 --- a/src/ApiResource/ApiMoney.php +++ b/src/ApiResource/ApiMoney.php @@ -2,6 +2,8 @@ namespace App\ApiResource; +use ApiPlatform\Metadata as API; +use App\Money\Conversion\Conversion; use App\Money\MoneyInterface; use Symfony\Component\Validator\Constraints as Assert; @@ -22,19 +24,28 @@ class ApiMoney implements MoneyInterface #[Assert\Currency()] public string $currency; + /** + * Conversion metadata. + */ + #[API\ApiProperty(writable: false)] + public ?array $conversion; + public function __construct( int $amount, string $currency, + ?Conversion $conversion = null, ) { $this->amount = $amount; $this->currency = $currency; + $this->conversion = $conversion?->toArray(); } public static function of(MoneyInterface $moneyInterface): self { return new self( $moneyInterface->getAmount(), - $moneyInterface->getCurrency() + $moneyInterface->getCurrency(), + $moneyInterface->getConversion() ); } @@ -47,4 +58,11 @@ class ApiMoney implements MoneyInterface { return $this->currency; } + + public function getConversion(): ?Conversion + { + return $this->conversion + ? Conversion::fromArray($this->conversion) + : null; + } } diff --git a/src/Entity/EmbeddableMoney.php b/src/Entity/EmbeddableMoney.php index 9f79eae..7fe9ef0 100644 --- a/src/Entity/EmbeddableMoney.php +++ b/src/Entity/EmbeddableMoney.php @@ -2,6 +2,7 @@ namespace App\Entity; +use App\Money\Conversion\Conversion; use App\Money\MoneyInterface; use Doctrine\ORM\Mapping as ORM; @@ -27,19 +28,25 @@ class EmbeddableMoney implements MoneyInterface #[ORM\Column(type: 'string', nullable: true)] public readonly ?string $currency; + #[ORM\Column(type: 'json', nullable: true)] + private readonly ?array $conversion; + public function __construct( ?int $amount = null, ?string $currency = null, + ?Conversion $conversion = null, ) { $this->amount = $amount; $this->currency = $currency; + $this->conversion = $conversion?->toArray(); } public static function of(MoneyInterface $moneyInterface): self { return new self( $moneyInterface->getAmount(), - $moneyInterface->getCurrency() + $moneyInterface->getCurrency(), + $moneyInterface->getConversion() ); } @@ -52,4 +59,11 @@ class EmbeddableMoney implements MoneyInterface { return $this->currency; } + + public function getConversion(): ?Conversion + { + return $this->conversion + ? Conversion::fromArray($this->conversion) + : null; + } } diff --git a/src/Money/Conversion/Conversion.php b/src/Money/Conversion/Conversion.php new file mode 100644 index 0000000..3257d65 --- /dev/null +++ b/src/Money/Conversion/Conversion.php @@ -0,0 +1,137 @@ +rounding = $rounding; + } + + if ($roundingMode) { + $this->rounding = self::getRoundingName($roundingMode); + } + + if ($this->rounding === null) { + throw new \Exception('Conversion rounding cannot be null'); + } + } + + /** + * @return Money the original Money that was converted from + */ + public function getFrom(): Money + { + return $this->from; + } + + /** + * @return Money the conversion result Money + */ + public function getTo(): Money + { + return $this->to; + } + + /** + * @return float the conversion rate given by the exchange rate provider + */ + public function getRate(): float + { + return $this->rate; + } + + /** + * @return string the date of the rate as given by the exchange rate provider + */ + public function getDate(): string + { + return $this->date; + } + + /** + * @return string the ExchangeInterface implementation name + */ + public function getProvider(): string + { + return $this->provider; + } + + /** + * @return string the rounding mode used during conversion + */ + public function getRounding(): string + { + return $this->rounding; + } + + public static function getRoundingName(RoundingMode $rounding): string + { + return match ($rounding) { + RoundingMode::UP => 'up', + RoundingMode::DOWN => 'down', + RoundingMode::FLOOR => 'floor', + RoundingMode::CEILING => 'ceiling', + RoundingMode::HALF_UP => 'half_up', + RoundingMode::HALF_DOWN => 'half_down', + RoundingMode::HALF_FLOOR => 'half_floor', + RoundingMode::HALF_CEILING => 'half_ceiling', + RoundingMode::HALF_EVEN => 'half_even', + }; + } + + public function toArray(): array + { + return [ + 'from' => [ + 'amount' => $this->from->getAmount(), + 'currency' => $this->from->getCurrency(), + ], + 'to' => [ + 'amount' => $this->to->getAmount(), + 'currency' => $this->to->getCurrency(), + ], + 'rate' => $this->rate, + 'date' => $this->date, + 'provider' => $this->provider, + 'rounding' => $this->rounding, + ]; + } + + public static function fromArray(array $data): self + { + return new self( + from: new Money( + $data['from']['amount'], + $data['from']['currency'] + ), + to: new Money( + $data['to']['amount'], + $data['to']['currency'] + ), + rate: (float) $data['rate'], + date: $data['date'], + provider: $data['provider'], + rounding: $data['rounding'] ?? null, + ); + } +} diff --git a/src/Money/Money.php b/src/Money/Money.php index d714474..917ddab 100644 --- a/src/Money/Money.php +++ b/src/Money/Money.php @@ -2,11 +2,14 @@ namespace App\Money; +use App\Money\Conversion\Conversion; + class Money implements MoneyInterface { public function __construct( private int $amount, private string $currency, + private ?Conversion $conversion = null, ) {} public function getAmount(): int @@ -18,4 +21,9 @@ class Money implements MoneyInterface { return $this->currency; } + + public function getConversion(): ?Conversion + { + return $this->conversion; + } } diff --git a/src/Money/MoneyInterface.php b/src/Money/MoneyInterface.php index ed1f99a..5eaae53 100644 --- a/src/Money/MoneyInterface.php +++ b/src/Money/MoneyInterface.php @@ -2,6 +2,8 @@ namespace App\Money; +use App\Money\Conversion\Conversion; + interface MoneyInterface { /** @@ -13,4 +15,9 @@ interface MoneyInterface * @return string 3-letter ISO 4217 currency code */ public function getCurrency(): string; + + /** + * @return Conversion|null If this Money is the result of a currency conversion operation. Else null. + */ + public function getConversion(): ?Conversion; } diff --git a/src/Money/Totalization/TotalizedMoney.php b/src/Money/Totalization/TotalizedMoney.php index c1f46e8..3ea5839 100644 --- a/src/Money/Totalization/TotalizedMoney.php +++ b/src/Money/Totalization/TotalizedMoney.php @@ -2,6 +2,7 @@ namespace App\Money\Totalization; +use App\Money\Conversion\Conversion; use App\Money\MoneyInterface; class TotalizedMoney implements MoneyInterface @@ -22,6 +23,11 @@ class TotalizedMoney implements MoneyInterface return $this->currency; } + public function getConversion(): ?Conversion + { + return null; + } + /** * @return int the total number of items that were totalized to produce the Money */ From 424ab8c842da7a60c4e85e351f306fb9f3851ee0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 15:37:08 +0200 Subject: [PATCH 165/203] Update Support message type to text --- src/Entity/Project/Support.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index 6a7f414..093568d 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -9,6 +9,7 @@ use App\Repository\Project\SupportRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[MapProvider(EntityMapProvider::class)] @@ -42,7 +43,7 @@ class Support #[ORM\Column] private ?bool $anonymous = null; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(Types::TEXT, nullable: true)] private ?string $message = null; public function __construct() From 0a53b7fa40f58f972ec8e8227b0d8e9af8bfeaf7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 15:37:51 +0200 Subject: [PATCH 166/203] Update exchange interface --- src/Money/Conversion/ExchangeInterface.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Money/Conversion/ExchangeInterface.php b/src/Money/Conversion/ExchangeInterface.php index 781b320..3ac73e6 100644 --- a/src/Money/Conversion/ExchangeInterface.php +++ b/src/Money/Conversion/ExchangeInterface.php @@ -20,7 +20,7 @@ interface ExchangeInterface public function getWeight(): int; /** - * @param MoneyInterface $money The money to be converted + * @param MoneyInterface $from The money to be converted * @param string $toCurrency The currency to convert to * * @return MoneyInterface The converted MoneyInterface @@ -28,7 +28,7 @@ interface ExchangeInterface * @throws CurrencyConversionException If the exchange rate is not available */ public function convert( - MoneyInterface $money, + MoneyInterface $from, string $toCurrency, ?Context $context = null, RoundingMode $roundingMode = RoundingMode::UP, @@ -48,9 +48,9 @@ interface ExchangeInterface * @param string $fromCurrency The currency to convert from * @param string $toCurrency The currency to convert to * - * @return \DateTimeInterface The date and time at which the rate was last updated + * @return string The date and time at which the rate was last updated as given by the rate provider * * @throws CurrencyConversionException If the exchange rate is not available */ - public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface; + public function getConversionDate(string $fromCurrency, string $toCurrency): string; } From fbcc438620f6f17085770468d994560e8f552356 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 15:38:04 +0200 Subject: [PATCH 167/203] Centralize common exchange operations on abstract exchange --- .../Conversion/Exchange/AbstractExchange.php | 58 +++++++++++++++++++ .../Exchange/EuropeanCentralBankExchange.php | 48 +-------------- .../Exchange/FrankfurterExchange.php | 44 +------------- 3 files changed, 62 insertions(+), 88 deletions(-) create mode 100644 src/Money/Conversion/Exchange/AbstractExchange.php diff --git a/src/Money/Conversion/Exchange/AbstractExchange.php b/src/Money/Conversion/Exchange/AbstractExchange.php new file mode 100644 index 0000000..047a06e --- /dev/null +++ b/src/Money/Conversion/Exchange/AbstractExchange.php @@ -0,0 +1,58 @@ +converter->convert( + MoneyService::toBrick($from), + $toCurrency, + $context, + $roundingMode + ); + + return new Money( + $converted->getMinorAmount()->toInt(), + $converted->getCurrency()->getCurrencyCode(), + new Conversion( + $from, + MoneyService::toMoney($converted), + rate: $this->getConversionRate($from->getCurrency(), $toCurrency), + date: $this->getConversionDate($from->getCurrency(), $toCurrency), + provider: $this->getName(), + roundingMode: $roundingMode + ) + ); + } + + public function getConversionRate(string $fromCurrency, string $toCurrency): float + { + return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); + } + + public function getConversionDate(string $fromCurrency, string $toCurrency): string + { + return $this->date; + } +} diff --git a/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php index 2b1cf84..103c026 100644 --- a/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php +++ b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php @@ -2,14 +2,7 @@ namespace App\Money\Conversion\Exchange; -use App\Money\Conversion\ExchangeInterface; -use App\Money\Money; -use App\Money\MoneyInterface; -use App\Money\MoneyService; -use Brick\Math\RoundingMode; -use Brick\Money\Context; use Brick\Money\CurrencyConverter; -use Brick\Money\ExchangeRateProvider; use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider; use Brick\Money\ExchangeRateProvider\ConfigurableProvider; use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -21,7 +14,7 @@ use Symfony\Contracts\Cache\ItemInterface; * * @see https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html */ -class EuropeanCentralBankExchange implements ExchangeInterface +class EuropeanCentralBankExchange extends AbstractExchange { public const NAME = 'european_central_bank'; public const WEIGHT = 100; @@ -41,12 +34,6 @@ class EuropeanCentralBankExchange implements ExchangeInterface private CacheInterface $cache; - private \DateTimeInterface $date; - - private ExchangeRateProvider $provider; - - private CurrencyConverter $converter; - public function __construct() { $this->cache = new FilesystemAdapter(); @@ -59,7 +46,7 @@ class EuropeanCentralBankExchange implements ExchangeInterface $provider->setExchangeRate(self::ISO_4217, $rate['currency'], $rate['rate']); } - $this->date = $this->parseECBTime($data['@attributes']['time']); + $this->date = $data['@attributes']['time']; $this->provider = new BaseCurrencyProvider($provider, self::ISO_4217); $this->converter = new CurrencyConverter($this->provider); } @@ -74,37 +61,6 @@ class EuropeanCentralBankExchange implements ExchangeInterface return self::WEIGHT; } - public function convert( - MoneyInterface $money, - string $toCurrency, - ?Context $context = null, - RoundingMode $roundingMode = RoundingMode::UP, - ): MoneyInterface { - $converted = $this->converter->convert( - MoneyService::toBrick($money), - $toCurrency, - $context, - $roundingMode - ); - - return new Money( - $converted->getMinorAmount()->toInt(), - $converted->getCurrency()->getCurrencyCode() - ); - } - - public function getConversionRate(string $fromCurrency, string $toCurrency): float - { - return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); - } - - public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface - { - $this->provider->getExchangeRate($fromCurrency, $toCurrency); - - return $this->date; - } - public function getData(): array { try { diff --git a/src/Money/Conversion/Exchange/FrankfurterExchange.php b/src/Money/Conversion/Exchange/FrankfurterExchange.php index 93e59bd..e302c75 100644 --- a/src/Money/Conversion/Exchange/FrankfurterExchange.php +++ b/src/Money/Conversion/Exchange/FrankfurterExchange.php @@ -2,19 +2,12 @@ namespace App\Money\Conversion\Exchange; -use App\Money\Conversion\ExchangeInterface; -use App\Money\Money; -use App\Money\MoneyInterface; -use App\Money\MoneyService; -use Brick\Math\RoundingMode; -use Brick\Money\Context; use Brick\Money\CurrencyConverter; -use Brick\Money\ExchangeRateProvider; use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider; use Brick\Money\ExchangeRateProvider\ConfigurableProvider; use Symfony\Contracts\HttpClient\HttpClientInterface; -class FrankfurterExchange implements ExchangeInterface +class FrankfurterExchange extends AbstractExchange { private const NAME = 'frankfurter'; private const WEIGHT = 200; @@ -22,10 +15,6 @@ class FrankfurterExchange implements ExchangeInterface private const ENDPOINT = 'https://api.frankfurter.dev/v1/latest'; private const ISO_4217 = 'EUR'; - private ExchangeRateProvider $provider; - private CurrencyConverter $converter; - private \DateTimeImmutable $date; - public function getName(): string { return self::NAME; @@ -47,37 +36,8 @@ class FrankfurterExchange implements ExchangeInterface $provider->setExchangeRate(self::ISO_4217, $currency, $rate); } - $this->date = new \DateTimeImmutable($data['date']); + $this->date = $data['date']; $this->provider = new BaseCurrencyProvider($provider, self::ISO_4217); $this->converter = new CurrencyConverter($this->provider); } - - public function convert( - MoneyInterface $money, - string $toCurrency, - ?Context $context = null, - RoundingMode $roundingMode = RoundingMode::UP, - ): MoneyInterface { - $converted = $this->converter->convert( - MoneyService::toBrick($money), - $toCurrency, - $context, - $roundingMode - ); - - return new Money( - $converted->getMinorAmount()->toInt(), - $converted->getCurrency()->getCurrencyCode() - ); - } - - public function getConversionRate(string $fromCurrency, string $toCurrency): float - { - return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); - } - - public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface - { - return $this->date; - } } From 99bdb0398194d09eacab20ca8f092c393b5b750b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 16:06:11 +0200 Subject: [PATCH 168/203] Perform conversion of to-be-charged amounts previous to Gateway processing --- src/Money/Conversion/Conversion.php | 5 +-- src/State/Gateway/CheckoutStateProcessor.php | 34 ++++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Money/Conversion/Conversion.php b/src/Money/Conversion/Conversion.php index 3257d65..8d2107d 100644 --- a/src/Money/Conversion/Conversion.php +++ b/src/Money/Conversion/Conversion.php @@ -3,6 +3,7 @@ namespace App\Money\Conversion; use App\Money\Money; +use App\Money\MoneyInterface; use Brick\Math\RoundingMode; class Conversion @@ -15,8 +16,8 @@ class Conversion * @param ?RoundingMode $roundingMode if supplied, then `rounding` will be set from here */ public function __construct( - private Money $from, - private Money $to, + private MoneyInterface $from, + private MoneyInterface $to, private float $rate, private string $date, private string $provider, diff --git a/src/State/Gateway/CheckoutStateProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php index 7e09f56..1c86e3a 100644 --- a/src/State/Gateway/CheckoutStateProcessor.php +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -7,9 +7,12 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Gateway\CheckoutApiResource; use App\Dto\CheckoutUpdationDto; +use App\Entity\EmbeddableMoney; use App\Entity\Gateway\Checkout; use App\Gateway\GatewayLocator; use App\Mapping\AutoMapper; +use App\Money\Conversion\ExchangeLocator; +use Brick\Money\Context\CustomContext; use Symfony\Component\DependencyInjection\Attribute\Autowire; class CheckoutStateProcessor implements ProcessorInterface @@ -17,8 +20,9 @@ class CheckoutStateProcessor implements ProcessorInterface public function __construct( #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $innerProcessor, - private GatewayLocator $gatewayLocator, private AutoMapper $autoMapper, + private GatewayLocator $gatewayLocator, + private ExchangeLocator $exchangeLocator, ) {} /** @@ -33,13 +37,31 @@ class CheckoutStateProcessor implements ProcessorInterface } $entity = $this->autoMapper->map($data, Checkout::class); - $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); - if ($data instanceof CheckoutApiResource) { - $entity = $this->gatewayLocator->get($data->gateway->name)->process($entity); - $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + /** @var Checkout */ + $checkout = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + + if ($data instanceof CheckoutUpdationDto) { + return $this->autoMapper->map($checkout, CheckoutApiResource::class); } - return $this->autoMapper->map($entity, CheckoutApiResource::class); + foreach ($checkout->getCharges() as $charge) { + $fromCurrency = $charge->getMoney()->getCurrency(); + $toCurrency = $charge->getTarget()->getCurrency(); + + if ($fromCurrency === $toCurrency) { + continue; + } + + $exchange = $this->exchangeLocator->get($fromCurrency, $toCurrency); + $exchanged = $exchange->convert($charge->getMoney(), $toCurrency, new CustomContext(0, 1)); + + $charge->setMoney(EmbeddableMoney::of($exchanged)); + } + + $checkout = $this->gatewayLocator->get($data->gateway->name)->process($checkout); + $checkout = $this->innerProcessor->process($checkout, $operation, $uriVariables, $context); + + return $this->autoMapper->map($checkout, CheckoutApiResource::class); } } From 0829fc6dc2f11d65d6f162157aca9f41592d4f2b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 17:34:40 +0200 Subject: [PATCH 169/203] Add listener to enforce transactions do not target accountings in different currencies --- .../TransactionCurrencyListener.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/EventListener/TransactionCurrencyListener.php diff --git a/src/EventListener/TransactionCurrencyListener.php b/src/EventListener/TransactionCurrencyListener.php new file mode 100644 index 0000000..642e3dd --- /dev/null +++ b/src/EventListener/TransactionCurrencyListener.php @@ -0,0 +1,26 @@ +getMoney(); + $target = $transaction->getTarget(); + + if ($money->getCurrency() !== $target->getCurrency()) { + throw new \LogicException(\sprintf( + "The Accounting with ID %s can only receive Transactions in %s currency. Please do a conversion operation before targeting an Accounting in a different currency.", + $target->getId(), + $target->getCurrency() + )); + } + } +} From 42457c5ad935afcb708600384abd4fd32b673c7c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 17:47:21 +0200 Subject: [PATCH 170/203] Normalize support money in db --- .../Project/SupportApiResource.php | 1 - src/Entity/Project/Support.php | 17 +++++ .../GatewayCheckoutSupportsListener.php | 65 +++++++++++-------- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index 36baf00..c91086c 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -64,7 +64,6 @@ class SupportApiResource /** * The total monetary value of the Transactions going to the Project. */ - #[MapFrom(Support::class, transformer: SupportMoneyMapTransformer::class)] public ApiMoney $money; /** diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index 093568d..7a413dc 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -4,6 +4,7 @@ namespace App\Entity\Project; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; +use App\Entity\EmbeddableMoney; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\SupportRepository; use AutoMapper\Attribute\MapProvider; @@ -11,6 +12,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Embedded; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'project_support')] @@ -40,6 +42,9 @@ class Support #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; + #[Embedded(class: EmbeddableMoney::class)] + private ?EmbeddableMoney $money = null; + #[ORM\Column] private ?bool $anonymous = null; @@ -104,6 +109,18 @@ class Support return $this; } + public function getMoney(): ?EmbeddableMoney + { + return $this->money; + } + + public function setMoney(EmbeddableMoney $money): static + { + $this->money = $money; + + return $this; + } + public function isAnonymous(): ?bool { return $this->anonymous; diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 5d11ddd..247f64e 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -3,10 +3,14 @@ namespace App\EventListener; use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; +use App\Entity\EmbeddableMoney; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Project\Project; use App\Entity\Project\Support; +use App\Money\Money; +use App\Money\MoneyService; use App\Repository\Project\SupportRepository; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\EntityManagerInterface; @@ -22,6 +26,7 @@ class GatewayCheckoutSupportsListener { public function __construct( private SupportRepository $supportRepository, + private MoneyService $moneyService, ) {} public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void @@ -50,31 +55,6 @@ class GatewayCheckoutSupportsListener } } - /** - * Create ProjectSupport for the given data. - */ - private function createSupport(Project $project, Accounting $origin, array $transactions): Support - { - $projectSupport = $this->supportRepository->findOneBy([ - 'project' => $project->getId(), - 'origin' => $origin->getId(), - ]); - - if (!$projectSupport) { - $projectSupport = new Support(); - } - - $projectSupport->setProject($project); - $projectSupport->setOrigin($origin); - $projectSupport->setAnonymous(false); - - foreach ($transactions as $transaction) { - $projectSupport->addTransaction($transaction); - } - - return $projectSupport; - } - /** * @return Support[] */ @@ -104,9 +84,42 @@ class GatewayCheckoutSupportsListener $supports = []; foreach ($transactionsByProject as $projectId => $transactions) { - $supports[] = $this->createSupport($projects[$projectId], $origin, $transactions); + $supports[] = $this->getSupport($projects[$projectId], $origin, $transactions); } return $supports; } + + /** + * Create ProjectSupport for the given data. + * + * @param Transaction[] $transactions + */ + private function getSupport(Project $project, Accounting $origin, array $transactions): Support + { + /** @var Support|null */ + $projectSupport = $this->supportRepository->findOneBy([ + 'project' => $project->getId(), + 'origin' => $origin->getId(), + ]); + + if (!$projectSupport) { + $projectSupport = new Support(); + } + + $projectSupport->setProject($project); + $projectSupport->setOrigin($origin); + $projectSupport->setAnonymous(false); + + $money = new Money(0, $project->getAccounting()->getCurrency()); + foreach ($transactions as $transaction) { + $money = $this->moneyService->add($transaction->getMoney(), $money); + + $projectSupport->addTransaction($transaction); + } + + $projectSupport->setMoney(EmbeddableMoney::of($money)); + + return $projectSupport; + } } From 490012919c3e11c9e3ae56de3e617ef8b9352091 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 18:24:10 +0200 Subject: [PATCH 171/203] Minor bug fixes --- src/EventListener/GatewayCheckoutSupportsListener.php | 2 +- src/EventListener/TransactionCurrencyListener.php | 4 +++- src/Money/Totalization/TotalizerLocator.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 247f64e..5960518 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -92,7 +92,7 @@ class GatewayCheckoutSupportsListener /** * Create ProjectSupport for the given data. - * + * * @param Transaction[] $transactions */ private function getSupport(Project $project, Accounting $origin, array $transactions): Support diff --git a/src/EventListener/TransactionCurrencyListener.php b/src/EventListener/TransactionCurrencyListener.php index 642e3dd..e3de416 100644 --- a/src/EventListener/TransactionCurrencyListener.php +++ b/src/EventListener/TransactionCurrencyListener.php @@ -1,5 +1,7 @@ getCurrency() !== $target->getCurrency()) { throw new \LogicException(\sprintf( - "The Accounting with ID %s can only receive Transactions in %s currency. Please do a conversion operation before targeting an Accounting in a different currency.", + 'The Accounting with ID %s can only receive Transactions in %s currency. Please do a conversion operation before targeting an Accounting in a different currency.', $target->getId(), $target->getCurrency() )); diff --git a/src/Money/Totalization/TotalizerLocator.php b/src/Money/Totalization/TotalizerLocator.php index aab931d..27aab85 100644 --- a/src/Money/Totalization/TotalizerLocator.php +++ b/src/Money/Totalization/TotalizerLocator.php @@ -5,7 +5,7 @@ namespace App\Money\Totalization; class TotalizerLocator { /** @var array */ - private array $totalizers; + private array $totalizers = []; public function __construct( iterable $interfaces, From cfcab835ccf6c14e09715b5f9403ea8ad5220580 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 18:24:37 +0200 Subject: [PATCH 172/203] Update TotalizerInterface to work directly with received items --- src/Money/Totalization/TotalizerInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Money/Totalization/TotalizerInterface.php b/src/Money/Totalization/TotalizerInterface.php index fe2240a..eba570c 100644 --- a/src/Money/Totalization/TotalizerInterface.php +++ b/src/Money/Totalization/TotalizerInterface.php @@ -10,7 +10,7 @@ interface TotalizerInterface public static function getSupportedResource(): string; /** - * @param array $filters an array of filters to be applied to the resource Collection + * @param iterable $items The resource Collection */ - public function totalize(array $filters): TotalizedMoney; + public function totalize(iterable $items): TotalizedMoney; } From 67720c40d26f0ecb487c78b3791dae4e2ea400e7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 18:24:56 +0200 Subject: [PATCH 173/203] Add MoneyTotalStateProvider --- config/services.yaml | 4 ++ src/State/MoneyTotalStateProvider.php | 88 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/State/MoneyTotalStateProvider.php diff --git a/config/services.yaml b/config/services.yaml index 0587b3d..d7d9d2c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -96,6 +96,10 @@ services: $hosts: - 'peertube.plataformess.org' + App\State\MoneyTotalStateProvider: + arguments: + - !tagged 'api_platform.doctrine.orm.query_extension.collection' + App\Money\MoneyService: arguments: - '%env(CURRENCY_DEFAULT)%' diff --git a/src/State/MoneyTotalStateProvider.php b/src/State/MoneyTotalStateProvider.php new file mode 100644 index 0000000..5638201 --- /dev/null +++ b/src/State/MoneyTotalStateProvider.php @@ -0,0 +1,88 @@ +getClass(); + if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { + $entityClass = $options->getEntityClass(); + } + + $totalizer = $this->totalizerLocator->get($entityClass); + + return $totalizer->totalize($this->getItems($entityClass, $operation, $uriVariables, $context)); + } + + private function getItems( + string $entityClass, + Operation $operation, + array $uriVariables = [], + array $context = [], + ): iterable { + /** @var EntityManagerInterface $manager */ + $manager = $this->managerRegistry->getManagerForClass($entityClass); + + $repository = $manager->getRepository($entityClass); + if (!method_exists($repository, 'createQueryBuilder')) { + throw new RuntimeException('The repository class must have a "createQueryBuilder" method.'); + } + + $queryBuilder = $repository->createQueryBuilder('o'); + $queryNameGenerator = new QueryNameGenerator(); + + $this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $entityClass, $operation); + + foreach ($this->collectionExtensions as $extension) { + $extension->applyToCollection($queryBuilder, $queryNameGenerator, $entityClass, $operation, $context); + + if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($entityClass, $operation, $context)) { + foreach ($extension->getResult($queryBuilder, $entityClass, $operation, $context) as $item) { + $manager->detach($item); + + yield $item; + } + + return; + } + } + + foreach ($queryBuilder->getQuery()->toIterable() as $item) { + $manager->detach($item); + + yield $item; + } + } +} From 6a8f9be8c949e75e95b5adb3b204d14dedec3764 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 18:25:07 +0200 Subject: [PATCH 174/203] Totalize money of Support --- .../Project/SupportApiResource.php | 8 +++- .../Totalizer/SupportTotalizer.php | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/Money/Totalization/Totalizer/SupportTotalizer.php diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index c91086c..7d42799 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -9,10 +9,9 @@ use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\Accounting\TransactionApiResource; use App\ApiResource\ApiMoney; use App\Entity\Project\Support; -use App\Mapping\Transformer\SupportMoneyMapTransformer; use App\State\ApiResourceStateProvider; +use App\State\MoneyTotalStateProvider; use App\State\Project\SupportStateProcessor; -use AutoMapper\Attribute\MapFrom; use Symfony\Component\Validator\Constraints as Assert; /** @@ -27,6 +26,11 @@ use Symfony\Component\Validator\Constraints as Assert; provider: ApiResourceStateProvider::class, )] #[API\GetCollection()] +#[API\GetCollection( + uriTemplate: '/project_supports/money_total', + provider: MoneyTotalStateProvider::class, + paginationEnabled: false, +)] #[API\Get()] #[API\Patch( security: 'is_granted("SUPPORT_EDIT", previous_object)', diff --git a/src/Money/Totalization/Totalizer/SupportTotalizer.php b/src/Money/Totalization/Totalizer/SupportTotalizer.php new file mode 100644 index 0000000..287168a --- /dev/null +++ b/src/Money/Totalization/Totalizer/SupportTotalizer.php @@ -0,0 +1,40 @@ + $items + */ + public function totalize(iterable $items): TotalizedMoney + { + $length = 0; + $money = null; + + foreach ($items as $support) { + ++$length; + $money = $this->moneyService->add( + $support->getMoney(), + $money ?? new Money(0, 'EUR') + ); + } + + return new TotalizedMoney($money->getAmount(), 'EUR', $length); + } +} From 88797a96d66f0fb1a7c47638f6e1cef943730025 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 18:53:17 +0200 Subject: [PATCH 175/203] Pump ProjectSupport with money --- src/Benzina/CheckoutsPump.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php index d10ce7e..37ec7e6 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/CheckoutsPump.php @@ -20,6 +20,8 @@ use App\Gateway\Gateway\DropGateway; use App\Gateway\Paypal\PaypalGateway; use App\Gateway\Stripe\StripeGateway; use App\Gateway\Wallet\WalletGateway; +use App\Money\Money; +use App\Money\MoneyService; use App\Repository\Project\ProjectRepository; use App\Repository\Project\SupportRepository; use App\Repository\TipjarRepository; @@ -57,6 +59,7 @@ class CheckoutsPump implements PumpInterface private SupportRepository $supportRepository, private TipjarRepository $tipjarRepository, private CheckoutService $checkoutService, + private MoneyService $moneyService, ) {} public function supports(mixed $sample): bool @@ -89,7 +92,7 @@ class CheckoutsPump implements PumpInterface $project = $this->getProject($record); $tipjar = $this->getPlatformTipjar(); - $invested = new \DateTime($record['invested']); + $invested = new \DateTime($record['datetime'] ?? $record['invested']); $checkout = new Checkout(); $checkout->setMigrated(true); @@ -154,6 +157,7 @@ class CheckoutsPump implements PumpInterface $support->setProject($project); $support->setOrigin($checkout->getOrigin()); $support->addTransaction($transaction); + $support->setMoney($this->calcSupportMoney($support)); $support->setAnonymous($this->getSupportAnon($record, $support)); $support->setMessage($this->getSupportMessage($record, $support, $context)); @@ -233,6 +237,20 @@ class CheckoutsPump implements PumpInterface return \sprintf("\n***\n%s", $msg['msg']); } + private function calcSupportMoney(Support $support): EmbeddableMoney + { + $money = null; + + foreach ($support->getTransactions() as $transaction) { + $money = $this->moneyService->add( + $transaction->getMoney(), + $money ?? new Money(0, $transaction->getMoney()->getCurrency()) + ); + } + + return EmbeddableMoney::of($money); + } + private function getPlatformTipjar(): Tipjar { $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); From 0aed9597d4160c8f26dc387ce44f65a15b1aa625 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 19:00:11 +0200 Subject: [PATCH 176/203] Rename InvestsPump --- src/Benzina/{CheckoutsPump.php => InvestsPump.php} | 4 ++-- src/Benzina/{CheckoutsPumpTrait.php => InvestsPumpTrait.php} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Benzina/{CheckoutsPump.php => InvestsPump.php} (99%) rename src/Benzina/{CheckoutsPumpTrait.php => InvestsPumpTrait.php} (96%) diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/InvestsPump.php similarity index 99% rename from src/Benzina/CheckoutsPump.php rename to src/Benzina/InvestsPump.php index 37ec7e6..250d4e4 100644 --- a/src/Benzina/CheckoutsPump.php +++ b/src/Benzina/InvestsPump.php @@ -31,12 +31,12 @@ use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; -class CheckoutsPump implements PumpInterface +class InvestsPump implements PumpInterface { use ArrayPumpTrait; use DatabasePumpTrait; use DoctrinePumpTrait; - use CheckoutsPumpTrait; + use InvestsPumpTrait; public const TRACKING_TITLE_V3 = 'v3 Invest ID'; public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment'; diff --git a/src/Benzina/CheckoutsPumpTrait.php b/src/Benzina/InvestsPumpTrait.php similarity index 96% rename from src/Benzina/CheckoutsPumpTrait.php rename to src/Benzina/InvestsPumpTrait.php index a2f6fa8..4aad917 100644 --- a/src/Benzina/CheckoutsPumpTrait.php +++ b/src/Benzina/InvestsPumpTrait.php @@ -2,7 +2,7 @@ namespace App\Benzina; -trait CheckoutsPumpTrait +trait InvestsPumpTrait { private const INVEST_KEYS = [ 'id', From cf923494f2b4e0d39c2bf4e91cafe7ce3cb023e6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 19:00:24 +0200 Subject: [PATCH 177/203] Specify /money_total output --- src/ApiResource/Project/SupportApiResource.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index c9bb6b5..dd2996f 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -10,6 +10,7 @@ use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\Accounting\TransactionApiResource; use App\ApiResource\ApiMoney; use App\Entity\Project\Support; +use App\Money\Totalization\TotalizedMoney; use App\State\ApiResourceStateProvider; use App\State\MoneyTotalStateProvider; use App\State\Project\SupportStateProcessor; @@ -30,6 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\GetCollection( uriTemplate: '/project_supports/money_total', provider: MoneyTotalStateProvider::class, + output: TotalizedMoney::class, paginationEnabled: false, )] #[API\Get()] From bde21f998069ed02e569643b276e0c83938dbadd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 21:10:13 +0200 Subject: [PATCH 178/203] A TotalizedMoney does not have to comply with MoneyInterface --- src/Money/Totalization/TotalizedMoney.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Money/Totalization/TotalizedMoney.php b/src/Money/Totalization/TotalizedMoney.php index 3ea5839..4479a21 100644 --- a/src/Money/Totalization/TotalizedMoney.php +++ b/src/Money/Totalization/TotalizedMoney.php @@ -2,10 +2,7 @@ namespace App\Money\Totalization; -use App\Money\Conversion\Conversion; -use App\Money\MoneyInterface; - -class TotalizedMoney implements MoneyInterface +class TotalizedMoney { public function __construct( private int $amount, @@ -23,11 +20,6 @@ class TotalizedMoney implements MoneyInterface return $this->currency; } - public function getConversion(): ?Conversion - { - return null; - } - /** * @return int the total number of items that were totalized to produce the Money */ From 43469232d4cd02afdfe5e38f3cab3621521aa9ab Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Sun, 7 Sep 2025 21:10:35 +0200 Subject: [PATCH 179/203] Add openapi description to the get money total operation --- src/ApiResource/Project/SupportApiResource.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ApiResource/Project/SupportApiResource.php b/src/ApiResource/Project/SupportApiResource.php index dd2996f..ec8d1c9 100644 --- a/src/ApiResource/Project/SupportApiResource.php +++ b/src/ApiResource/Project/SupportApiResource.php @@ -33,6 +33,22 @@ use Symfony\Component\Validator\Constraints as Assert; provider: MoneyTotalStateProvider::class, output: TotalizedMoney::class, paginationEnabled: false, + openapi: new \ApiPlatform\OpenApi\Model\Operation( + summary: 'Get money total', + description: 'Returns a single TotalizedMoney object', + responses: [ + '200' => [ + 'description' => 'Totalized money', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/ProjectSupport.TotalizedMoney', + ], + ], + ], + ], + ] + ) )] #[API\Get()] #[API\Patch( From 6092807ca6b983574b32808c832a5634a757ee03 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 00:56:08 +0200 Subject: [PATCH 180/203] Fix bad money class asignation --- src/State/Accounting/AccountingBalanceStateProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/State/Accounting/AccountingBalanceStateProvider.php b/src/State/Accounting/AccountingBalanceStateProvider.php index b34cd72..ca4b0f8 100644 --- a/src/State/Accounting/AccountingBalanceStateProvider.php +++ b/src/State/Accounting/AccountingBalanceStateProvider.php @@ -6,6 +6,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\Accounting\AccountingBalance; +use App\ApiResource\ApiMoney; use App\Mapping\AutoMapper; use App\Repository\Accounting\AccountingRepository; use App\Service\AccountingService; @@ -35,7 +36,7 @@ class AccountingBalanceStateProvider implements ProviderInterface $resource = $this->autoMapper->map($accounting, AccountingApiResource::class); $balance->accounting = $resource; - $balance->balance = $this->accountingService->calcBalance($accounting); + $balance->balance = ApiMoney::of($this->accountingService->calcBalance($accounting)); return $balance; } From fcf043f5649dd32608a40e2666a6ec87d6896100 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 00:56:50 +0200 Subject: [PATCH 181/203] Remove unused SupportMoney map transformer --- .../SupportMoneyMapTransformer.php | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/Mapping/Transformer/SupportMoneyMapTransformer.php diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php deleted file mode 100644 index a6cccd2..0000000 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ /dev/null @@ -1,31 +0,0 @@ -getProject()->getAccounting()->getCurrency()); - - $transactions = $source->getTransactions(); - foreach ($transactions as $transaction) { - $money = $this->moneyService->add($transaction->getMoney(), $money); - } - - return ApiMoney::of($money); - } -} From 50540ce3940a7142af988e5301ca9796c31efe59 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 01:45:08 +0200 Subject: [PATCH 182/203] Remove conversion from writable context in ApiMoney --- src/ApiResource/ApiMoney.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ApiResource/ApiMoney.php b/src/ApiResource/ApiMoney.php index c22e034..3d56faf 100644 --- a/src/ApiResource/ApiMoney.php +++ b/src/ApiResource/ApiMoney.php @@ -27,26 +27,27 @@ class ApiMoney implements MoneyInterface /** * Conversion metadata. */ - #[API\ApiProperty(writable: false)] + #[API\ApiProperty(readable: true, writable: false)] public ?array $conversion; public function __construct( int $amount, string $currency, - ?Conversion $conversion = null, ) { $this->amount = $amount; $this->currency = $currency; - $this->conversion = $conversion?->toArray(); } public static function of(MoneyInterface $moneyInterface): self { - return new self( + $money = new self( $moneyInterface->getAmount(), $moneyInterface->getCurrency(), - $moneyInterface->getConversion() ); + + $money->conversion = $moneyInterface->getConversion()?->toArray(); + + return $money; } public function getAmount(): int From 3eb38fd9dfb1ead0c84a9f367b8c936e4a7cd648 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 02:07:47 +0200 Subject: [PATCH 183/203] Update Reward data schema --- src/ApiResource/Project/RewardApiResource.php | 28 +++++++++++++------ src/Entity/Project/Reward.php | 28 +++++++++++++++---- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index eb59693..4ea9ca9 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -64,22 +64,32 @@ class RewardApiResource #[Assert\NotNull()] #[Assert\Type('bool')] #[API\ApiFilter(BooleanFilter::class)] - public bool $hasUnits; + public bool $isFinite = false; /** - * For finite rewards, the total amount of existing unitsTotal.\ - * Required if `hasUnits`. + * For finite rewards, the total amount of existing units.\ + * Required if `isFinite`. */ - #[Assert\When( - 'this.hasUnits == true', - constraints: [new Assert\Positive()] - )] - public int $unitsTotal = 0; + #[Assert\When('this.isFinite == true', [new Assert\Positive()])] + public ?int $unitsTotal = null; /** - * For finite rewards, the currently available amount of unitsTotal that can be claimed. + * The total amount of claimed units. + */ + #[API\ApiProperty(writable: false)] + #[API\ApiFilter(OrderFilter::class)] + public int $unitsClaimed = 0; + + /** + * For finite rewards, the currently available amount of units that can be claimed. */ #[API\ApiProperty(writable: false)] #[API\ApiFilter(OrderFilter::class)] public int $unitsAvailable = 0; + + /** + * @var RewardClaimApiResource[] + */ + #[API\ApiProperty(writable: false)] + public array $claims; } diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index 6ea2a3b..6428f21 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -53,7 +53,7 @@ class Reward implements LocalizedEntityInterface * Rewards might be finite, i.e: has a limited amount of existing unitsTotal. */ #[ORM\Column] - private ?bool $hasUnits = null; + private ?bool $isFinite = null; /** * For finite rewards, the total amount of existing unitsTotal. @@ -61,6 +61,12 @@ class Reward implements LocalizedEntityInterface #[ORM\Column] private ?int $unitsTotal = null; + /** + * The total amount of claimed rewards. + */ + #[ORM\Column] + private ?int $unitsClaimed = null; + /** * For finite rewards, the currently available amount of unitsTotal that can be claimed. */ @@ -131,14 +137,14 @@ class Reward implements LocalizedEntityInterface return $this; } - public function hasUnits(): bool + public function isFinite(): bool { - return $this->hasUnits; + return $this->isFinite; } - public function setHasUnits(bool $hasUnits): static + public function setIsFinite(bool $isFinite): static { - $this->hasUnits = $hasUnits; + $this->isFinite = $isFinite; return $this; } @@ -155,6 +161,18 @@ class Reward implements LocalizedEntityInterface return $this; } + public function getUnitsClaimed(): ?int + { + return $this->unitsClaimed; + } + + public function setUnitsClaimed(int $unitsClaimed): static + { + $this->unitsClaimed = $unitsClaimed; + + return $this; + } + public function getUnitsAvailable(): ?int { return $this->unitsAvailable; From 1d1d40055614b6cd348139dd8faff011c7fbd52f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 12:34:05 +0200 Subject: [PATCH 184/203] Charge shall contain migrated information too --- src/Benzina/InvestsPump.php | 4 ++++ src/Entity/Gateway/Charge.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/Benzina/InvestsPump.php b/src/Benzina/InvestsPump.php index 250d4e4..a3b6380 100644 --- a/src/Benzina/InvestsPump.php +++ b/src/Benzina/InvestsPump.php @@ -110,6 +110,8 @@ class InvestsPump implements PumpInterface } $charge = new Charge(); + $charge->setMigrated(true); + $charge->getMigratedId($record['id']); $charge->setDateCreated($invested); $charge->setDateUpdated(new \DateTime()); $charge->setStatus($this->getChargeStatus($record)); @@ -128,6 +130,8 @@ class InvestsPump implements PumpInterface if ($record['donate_amount'] > 0) { $tip = new Charge(); + $tip->setMigrated(true); + $tip->setMigratedId($record['id']); $tip->setDateCreated($invested); $tip->setDateUpdated(new \DateTime()); $tip->setStatus($this->getChargeStatus($record)); diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index cd52fc1..a81ca5d 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -5,6 +5,7 @@ namespace App\Entity\Gateway; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; use App\Entity\EmbeddableMoney as Money; +use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\ChargeStatus; @@ -25,9 +26,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[MapProvider(EntityMapProvider::class)] #[Gedmo\Loggable()] #[ORM\Table(name: 'checkout_charge')] +#[ORM\Index(fields: ['migratedId'])] #[ORM\Entity(repositoryClass: ChargeRepository::class)] class Charge { + use MigratedEntity; use TimestampedCreationEntity; use TimestampedUpdationEntity; From e05e0c52e609fca79f31af8bf5ddcf74ff1cd063 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 12:44:43 +0200 Subject: [PATCH 185/203] Bump api-platform versions --- composer.lock | 295 ++++++++++++++++++++++++++------------------------ symfony.lock | 9 ++ 2 files changed, 164 insertions(+), 140 deletions(-) diff --git a/composer.lock b/composer.lock index 18ccbb0..32fb42a 100644 --- a/composer.lock +++ b/composer.lock @@ -8,24 +8,24 @@ "packages": [ { "name": "api-platform/doctrine-common", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "c706b23d51936ff45b45f549b81861391f967f90" + "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/c706b23d51936ff45b45f549b81861391f967f90", - "reference": "c706b23d51936ff45b45f549b81861391f967f90", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7", + "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/collections": "^2.1", "doctrine/common": "^3.2.2", - "doctrine/persistence": "^3.2", + "doctrine/persistence": "^3.2 || ^4.0", "php": ">=8.2" }, "conflict": { @@ -35,7 +35,7 @@ "doctrine/mongodb-odm": "^2.10", "doctrine/orm": "^2.17 || ^3.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "suggest": { "api-platform/graphql": "For GraphQl mercure subscriptions.", @@ -56,6 +56,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -89,28 +90,28 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.1.7" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-08-18T13:30:43+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2" + "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2", - "reference": "4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/61a199da6f6014dba2da43ea1a66b2c9dda27263", + "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263", "shasum": "" }, "require": { - "api-platform/doctrine-common": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/doctrine-common": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/orm": "^2.17 || ^3.0", "php": ">=8.2", "symfony/property-info": "^6.4 || ^7.1" @@ -118,7 +119,7 @@ "require-dev": { "doctrine/doctrine-bundle": "^2.11", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", "symfony/cache": "^6.4 || ^7.0", @@ -140,6 +141,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -173,30 +175,30 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.1.7" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/documentation", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", - "reference": "20334b3b29dca992e3520685f33f75f7fcba8da6" + "reference": "1a0ac988d659008ef8667d05bc9978863026bab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/documentation/zipball/20334b3b29dca992e3520685f33f75f7fcba8da6", - "reference": "20334b3b29dca992e3520685f33f75f7fcba8da6", + "url": "https://api.github.com/repos/api-platform/documentation/zipball/1a0ac988d659008ef8667d05bc9978863026bab8", + "reference": "1a0ac988d659008ef8667d05bc9978863026bab8", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "project", "extra": { @@ -209,6 +211,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -234,34 +237,34 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.1.7" + "source": "https://github.com/api-platform/documentation/tree/v4.2.0-alpha.1" }, - "time": "2025-02-28T10:08:08+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "2eff0c4ae6320426586db4ce5dcd730f51665ea5" + "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/2eff0c4ae6320426586db4ce5dcd730f51665ea5", - "reference": "2eff0c4ae6320426586db4ce5dcd730f51665ea5", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/f65f092c90311a87ebb6dda87db3ca08b57c10d6", + "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/http-foundation": "^6.4 || ^7.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.0 || ^7.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/http-client": "^6.4 || ^7.0" }, @@ -276,6 +279,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -311,31 +315,31 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.1.7" + "source": "https://github.com/api-platform/http-cache/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/hydra", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "f05dbc5ed805859ef4335fd844493b10fd857046" + "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/f05dbc5ed805859ef4335fd844493b10fd857046", - "reference": "f05dbc5ed805859ef4335fd844493b10fd857046", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/8c75b814af143c95ffc1857565169ff5b6f1b421", + "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1", - "api-platform/json-schema": "^4.1", - "api-platform/jsonld": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", + "api-platform/documentation": "^4.1.11", + "api-platform/json-schema": "^4.1.11", + "api-platform/jsonld": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/web-link": "^6.4 || ^7.1" }, @@ -345,7 +349,7 @@ "api-platform/doctrine-orm": "^4.1", "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -358,6 +362,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -395,26 +400,26 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.1.7" + "source": "https://github.com/api-platform/hydra/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-15T14:10:59+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "32bf153319c1d4778f6daafe00d636508ee2c4d9" + "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/32bf153319c1d4778f6daafe00d636508ee2c4d9", - "reference": "32bf153319c1d4778f6daafe00d636508ee2c4d9", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f", + "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2", "symfony/console": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -423,7 +428,7 @@ }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -436,6 +441,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -473,32 +479,32 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.1.7" + "source": "https://github.com/api-platform/json-schema/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-29T12:24:14+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "ef2112bd65a49761efe4253bbf1ba5328e307e00" + "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/ef2112bd65a49761efe4253bbf1ba5328e307e00", - "reference": "ef2112bd65a49761efe4253bbf1ba5328e307e00", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/e122bf1f04f895e80e6469e0f09d1f06f7508ca6", + "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -511,6 +517,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -550,40 +557,40 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.1.7" + "source": "https://github.com/api-platform/jsonld/tree/v4.1.23" }, - "time": "2025-04-10T12:08:09+00:00" + "time": "2025-07-25T10:05:30+00:00" }, { "name": "api-platform/metadata", - "version": "v4.1.7", + "version": "v4.1.18", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "8adff3619c680ff4cb69b77228ac62dd4e4e1cc3" + "reference": "5df40a760eef56963c151181dc9498bbc260e7ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/8adff3619c680ff4cb69b77228ac62dd4e4e1cc3", - "reference": "8adff3619c680ff4cb69b77228ac62dd4e4e1cc3", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/5df40a760eef56963c151181dc9498bbc260e7ae", + "reference": "5df40a760eef56963c151181dc9498bbc260e7ae", "shasum": "" }, "require": { - "doctrine/inflector": "^1.0 || ^2.0", + "doctrine/inflector": "^2.0", "php": ">=8.2", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/property-info": "^6.4 || ^7.1", "symfony/string": "^6.4 || ^7.0", - "symfony/type-info": "^7.1" + "symfony/type-info": "^7.2" }, "require-dev": { - "api-platform/json-schema": "^4.1", - "api-platform/openapi": "^4.1", - "api-platform/state": "^4.1", + "api-platform/json-schema": "^4.1.11", + "api-platform/openapi": "^4.1.11", + "api-platform/state": "^4.1.11", "phpspec/prophecy-phpunit": "^2.2", "phpstan/phpdoc-parser": "^1.29 || ^2.0", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/config": "^6.4 || ^7.0", "symfony/routing": "^6.4 || ^7.0", "symfony/var-dumper": "^6.4 || ^7.0", @@ -606,6 +613,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -646,28 +654,28 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.1.7" + "source": "https://github.com/api-platform/metadata/tree/v4.1.18" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-03T13:06:55+00:00" }, { "name": "api-platform/openapi", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "32e4591d18b19401f9c0aeb0965ffb178938b978" + "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/32e4591d18b19401f9c0aeb0965ffb178938b978", - "reference": "32e4591d18b19401f9c0aeb0965ffb178938b978", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/793b53e51a5c24076d4024b6aa77de29e74015cd", + "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd", "shasum": "" }, "require": { - "api-platform/json-schema": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/json-schema": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/console": "^6.4 || ^7.0", "symfony/filesystem": "^6.4 || ^7.0", @@ -679,7 +687,7 @@ "api-platform/doctrine-odm": "^4.1", "api-platform/doctrine-orm": "^4.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -692,6 +700,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -732,27 +741,27 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.1.7" + "source": "https://github.com/api-platform/openapi/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-29T08:53:27+00:00" }, { "name": "api-platform/serializer", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "132bca30db7f0074e93e36c48af2f13f33d91958" + "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/132bca30db7f0074e93e36c48af2f13f33d91958", - "reference": "132bca30db7f0074e93e36c48af2f13f33d91958", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/70dbdeac9584870be444d78c1a796b6edb9e46a5", + "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/property-access": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -767,7 +776,7 @@ "api-platform/openapi": "^4.1", "doctrine/collections": "^2.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/mercure-bundle": "*", "symfony/var-dumper": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0" @@ -787,6 +796,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -822,34 +832,35 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.1.7" + "source": "https://github.com/api-platform/serializer/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-08-29T15:13:26+00:00" }, { "name": "api-platform/state", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "ddae2e24bdfc83b69460662581aaa9f095523a0d" + "reference": "056b07285cdc904984fb44c2614f7df8f4620a95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/ddae2e24bdfc83b69460662581aaa9f095523a0d", - "reference": "ddae2e24bdfc83b69460662581aaa9f095523a0d", + "url": "https://api.github.com/repos/api-platform/state/zipball/056b07285cdc904984fb44c2614f7df8f4620a95", + "reference": "056b07285cdc904984fb44c2614f7df8f4620a95", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.18", "php": ">=8.2", "psr/container": "^1.0 || ^2.0", - "symfony/http-kernel": "^6.4 || ^7.0" + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "symfony/translation-contracts": "^3.0" }, "require-dev": { - "api-platform/serializer": "^4.1", "api-platform/validator": "^4.1", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/http-foundation": "^6.4 || ^7.0", "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.1" @@ -872,6 +883,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -912,35 +924,35 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.1.7" + "source": "https://github.com/api-platform/state/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-16T14:01:52+00:00" }, { "name": "api-platform/symfony", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "8e295045e1c036efaa7bf64213de90365139e68c" + "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/8e295045e1c036efaa7bf64213de90365139e68c", - "reference": "8e295045e1c036efaa7bf64213de90365139e68c", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1", + "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1", - "api-platform/http-cache": "^4.1", - "api-platform/hydra": "^4.1", - "api-platform/json-schema": "^4.1", - "api-platform/jsonld": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/openapi": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", - "api-platform/validator": "^4.1", + "api-platform/documentation": "^4.1.11", + "api-platform/http-cache": "^4.1.11", + "api-platform/hydra": "^4.1.11", + "api-platform/json-schema": "^4.1.11", + "api-platform/jsonld": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/openapi": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", + "api-platform/validator": "^4.1.11", "php": ">=8.2", "symfony/property-access": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -956,7 +968,7 @@ "api-platform/graphql": "^4.1", "api-platform/parameter-validator": "^3.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/expression-language": "^6.4 || ^7.0", "symfony/mercure-bundle": "*", "symfony/routing": "^6.4 || ^7.0", @@ -994,6 +1006,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -1035,35 +1048,36 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.1.7" + "source": "https://github.com/api-platform/symfony/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-09-05T07:30:37+00:00" }, { "name": "api-platform/validator", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", - "reference": "a7052710c60ca2c85a10b3e1d841f71d3e316fa8" + "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/a7052710c60ca2c85a10b3e1d841f71d3e316fa8", - "reference": "a7052710c60ca2c85a10b3e1d841f71d3e316fa8", + "url": "https://api.github.com/repos/api-platform/validator/zipball/9f0bde95dccf1d86e6a6165543d601a4a46eaa9a", + "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/type-info": "^7.2", + "symfony/validator": "^6.4 || ^7.1", "symfony/web-link": "^6.4 || ^7.1" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -1076,6 +1090,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -1108,9 +1123,9 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.1.7" + "source": "https://github.com/api-platform/validator/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-16T14:01:52+00:00" }, { "name": "beberlei/doctrineextensions", @@ -12580,7 +12595,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -12588,6 +12603,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-dev": [], + "plugin-api-version": "2.2.0" } diff --git a/symfony.lock b/symfony.lock index 13d0289..0c70376 100644 --- a/symfony.lock +++ b/symfony.lock @@ -36,6 +36,15 @@ "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" } }, + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, "doctrine/doctrine-bundle": { "version": "2.12", "recipe": { From 2d88bdc7c16faab452cae5c7a4c7d0b5fb3357f4 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 12:49:28 +0200 Subject: [PATCH 186/203] Require goteo/benzina:^0.5 --- composer.json | 2 +- composer.lock | 722 ++++++++++++++++++++++++++++---------------------- 2 files changed, 399 insertions(+), 325 deletions(-) diff --git a/composer.json b/composer.json index 28a8e03..cb4fe69 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", "doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4", - "goteo/benzina": "^0.4", + "goteo/benzina": "^0.5", "jolicode/automapper": "^9.2", "mpratt/embera": "~2.0", "nelmio/cors-bundle": "^2.3", diff --git a/composer.lock b/composer.lock index 32fb42a..3b63876 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f049d2e637e75cf873dddba113d9a2ab", + "content-hash": "230c17be8429a245ab35233cc148b538", "packages": [ { "name": "api-platform/doctrine-common", @@ -1512,99 +1512,6 @@ }, "time": "2024-09-05T10:17:24+00:00" }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], - "time": "2022-05-20T20:07:39+00:00" - }, { "name": "doctrine/collections", "version": "2.3.0", @@ -1784,36 +1691,39 @@ }, { "name": "doctrine/dbal", - "version": "3.9.4", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", "doctrine/event-manager": "^1|^2", "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, + "conflict": { + "doctrine/cache": "< 1.11" + }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.1", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.1", + "phpstan/phpstan": "2.1.22", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.22", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -1875,7 +1785,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.4" + "source": "https://github.com/doctrine/dbal/tree/3.10.2" }, "funding": [ { @@ -1891,7 +1801,7 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:28:55+00:00" + "time": "2025-09-04T23:51:27+00:00" }, { "name": "doctrine/deprecations", @@ -2242,33 +2152,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2313,7 +2222,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -2329,7 +2238,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/instantiator", @@ -2583,16 +2492,16 @@ }, { "name": "doctrine/orm", - "version": "3.3.2", + "version": "3.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "c9557c588b3a70ed93caff069d0aa75737f25609" + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/c9557c588b3a70ed93caff069d0aa75737f25609", - "reference": "c9557c588b3a70ed93caff069d0aa75737f25609", + "url": "https://api.github.com/repos/doctrine/orm/zipball/5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", "shasum": "" }, "require": { @@ -2612,7 +2521,7 @@ "symfony/var-exporter": "^6.3.9 || ^7.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^13.0", "phpbench/phpbench": "^1.0", "phpdocumentor/guides-cli": "^1.4", "phpstan/extension-installer": "^1.4", @@ -2620,7 +2529,7 @@ "phpstan/phpstan-deprecation-rules": "^2", "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.2", + "squizlabs/php_codesniffer": "3.12.0", "symfony/cache": "^5.4 || ^6.2 || ^7.0" }, "suggest": { @@ -2667,40 +2576,37 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.3.2" + "source": "https://github.com/doctrine/orm/tree/3.5.2" }, - "time": "2025-02-04T19:43:15+00:00" + "time": "2025-08-08T17:00:40+00:00" }, { "name": "doctrine/persistence", - "version": "3.4.0", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + "reference": "dcbdfe4b211ae09478e192289cae7ab0987b29a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/dcbdfe4b211ae09478e192289cae7ab0987b29a4", + "reference": "dcbdfe4b211ae09478e192289cae7ab0987b29a4", "shasum": "" }, "require": { "doctrine/event-manager": "^1 || ^2", - "php": "^7.2 || ^8.0", + "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" }, - "conflict": { - "doctrine/common": "<2.10" - }, "require-dev": { "doctrine/coding-standard": "^12", - "doctrine/common": "^3.0", "phpstan/phpstan": "1.12.7", "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5.38 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0" }, "type": "library", "autoload": { @@ -2749,7 +2655,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.4.0" + "source": "https://github.com/doctrine/persistence/tree/4.1.0" }, "funding": [ { @@ -2765,7 +2671,7 @@ "type": "tidelift" } ], - "time": "2024-10-30T19:48:12+00:00" + "time": "2025-08-21T16:00:31+00:00" }, { "name": "doctrine/sql-formatter", @@ -3086,22 +2992,23 @@ }, { "name": "goteo/benzina", - "version": "0.4", + "version": "0.5", "source": { "type": "git", "url": "https://github.com/goteo/benzina.git", - "reference": "ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae" + "reference": "9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goteo/benzina/zipball/ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae", - "reference": "ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae", + "url": "https://api.github.com/repos/goteo/benzina/zipball/9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62", + "reference": "9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62", "shasum": "" }, "require": { "doctrine/orm": "^3.3", "ext-ctype": "*", "ext-iconv": "*", + "ocramius/doctrine-batch-utils": "^2.10", "php": ">=8.1", "symfony/console": "6.4.*", "symfony/dotenv": "6.4.*", @@ -3140,9 +3047,9 @@ "description": "Migration tool to pump v3 data into a v4 schema", "support": { "issues": "https://github.com/goteo/benzina/issues", - "source": "https://github.com/goteo/benzina/tree/0.4" + "source": "https://github.com/goteo/benzina/tree/0.5" }, - "time": "2025-02-27T11:20:31+00:00" + "time": "2025-09-08T10:27:32+00:00" }, { "name": "jolicode/automapper", @@ -3524,6 +3431,70 @@ }, "time": "2024-12-30T11:07:19+00:00" }, + { + "name": "ocramius/doctrine-batch-utils", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/DoctrineBatchUtils.git", + "reference": "35b164ea216da00ecccde6a0152f8be67bfb841f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/DoctrineBatchUtils/zipball/35b164ea216da00ecccde6a0152f8be67bfb841f", + "reference": "35b164ea216da00ecccde6a0152f8be67bfb841f", + "shasum": "" + }, + "require": { + "doctrine/orm": "^3.3.2", + "doctrine/persistence": "^4.0.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "doctrine/dbal": "^4.2.2", + "phpunit/phpunit": "^11.5.6", + "psalm/plugin-phpunit": "^0.19.2", + "roave/infection-static-analysis-plugin": "^1.36.0", + "vimeo/psalm": "^6.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "DoctrineBatchUtils\\": "src/DoctrineBatchUtils" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.io/" + } + ], + "description": "A set of utilities to operate with Doctrine ORM's batch processing functionality", + "homepage": "https://github.com/Ocramius/DoctrineBatchUtils", + "keywords": [ + "batch processing", + "doctrine", + "doctrine2", + "orm" + ], + "support": { + "issues": "https://github.com/Ocramius/DoctrineBatchUtils/issues", + "source": "https://github.com/Ocramius/DoctrineBatchUtils/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + } + ], + "time": "2025-02-05T12:04:35+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.0.0", @@ -4645,16 +4616,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.21", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee" + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/d1abcf763a7414f2e572f676f22da7a06c8cd9ee", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee", + "url": "https://api.github.com/repos/symfony/cache/zipball/d038cd3054aeaf1c674022a77048b2ef6376a175", + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175", "shasum": "" }, "require": { @@ -4721,7 +4692,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.21" + "source": "https://github.com/symfony/cache/tree/v6.4.24" }, "funding": [ { @@ -4732,25 +4703,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-08T08:21:20+00:00" + "time": "2025-07-30T09:32:03+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", "shasum": "" }, "require": { @@ -4764,7 +4739,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4797,7 +4772,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" }, "funding": [ { @@ -4813,7 +4788,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-03-13T15:25:07+00:00" }, { "name": "symfony/clock", @@ -4891,16 +4866,16 @@ }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/80e2cf005cf17138c97193be0434cdcfd1b2212e", + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e", "shasum": "" }, "require": { @@ -4946,7 +4921,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v6.4.24" }, "funding": [ { @@ -4957,25 +4932,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-04T11:33:53+00:00" + "time": "2025-07-26T13:50:30+00:00" }, { "name": "symfony/console", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719" + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719", + "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", "shasum": "" }, "require": { @@ -5040,7 +5019,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.21" + "source": "https://github.com/symfony/console/tree/v6.4.25" }, "funding": [ { @@ -5051,25 +5030,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-07T15:42:41+00:00" + "time": "2025-08-22T10:21:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.20", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "c49796a9184a532843e78e50df9e55708b92543a" + "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c49796a9184a532843e78e50df9e55708b92543a", - "reference": "c49796a9184a532843e78e50df9e55708b92543a", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/900da8a42eceeb4a13a0ec34caa7db49328daff3", + "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3", "shasum": "" }, "require": { @@ -5121,7 +5104,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.20" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.25" }, "funding": [ { @@ -5132,25 +5115,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-13T09:55:08+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -5163,7 +5150,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5188,7 +5175,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -5204,7 +5191,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/doctrine-bridge", @@ -5316,16 +5303,16 @@ }, { "name": "symfony/dotenv", - "version": "v6.4.16", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/234b6c602f12b00693f4b0d1054386fb30dfc8ff", + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff", "shasum": "" }, "require": { @@ -5370,7 +5357,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.16" + "source": "https://github.com/symfony/dotenv/tree/v6.4.24" }, "funding": [ { @@ -5381,25 +5368,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-27T11:08:19+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.20", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031" + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/aa3bcf4f7674719df078e61cc8062e5b7f752031", - "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c", "shasum": "" }, "require": { @@ -5445,7 +5436,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.20" + "source": "https://github.com/symfony/error-handler/tree/v6.4.24" }, "funding": [ { @@ -5456,25 +5447,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-01T13:00:38+00:00" + "time": "2025-07-24T08:25:04+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "b0cf3162020603587363f0551cd3be43958611ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", + "reference": "b0cf3162020603587363f0551cd3be43958611ff", "shasum": "" }, "require": { @@ -5525,7 +5520,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" }, "funding": [ { @@ -5536,25 +5531,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -5568,7 +5567,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5601,7 +5600,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -5617,7 +5616,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/expression-language", @@ -5685,16 +5684,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", "shasum": "" }, "require": { @@ -5731,7 +5730,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" }, "funding": [ { @@ -5742,25 +5741,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/finder", - "version": "v6.4.17", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + "reference": "73089124388c8510efb8d2d1689285d285937b08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", "shasum": "" }, "require": { @@ -5795,7 +5798,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.17" + "source": "https://github.com/symfony/finder/tree/v6.4.24" }, "funding": [ { @@ -5806,25 +5809,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-29T13:51:37+00:00" + "time": "2025-07-15T12:02:45+00:00" }, { "name": "symfony/flex", - "version": "v2.5.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" + "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "url": "https://api.github.com/repos/symfony/flex/zipball/f356aa35f3cf3d2f46c31d344c1098eb2d260426", + "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426", "shasum": "" }, "require": { @@ -5863,7 +5870,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.5.0" + "source": "https://github.com/symfony/flex/tree/v2.8.2" }, "funding": [ { @@ -5874,25 +5881,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-03T07:50:46+00:00" + "time": "2025-08-22T07:17:23+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a" + "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", - "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1d6a764b58e4f780df00f71c20ba3a61095ea447", + "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447", "shasum": "" }, "require": { @@ -6012,7 +6023,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.21" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.25" }, "funding": [ { @@ -6023,12 +6034,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T13:27:38+00:00" + "time": "2025-08-26T10:44:20+00:00" }, { "name": "symfony/http-client", @@ -6203,16 +6218,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99" + "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f0c7ea41db479383b81d436b836d37168fd5b99", - "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6bc974c0035b643aa497c58d46d9e25185e4b272", + "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272", "shasum": "" }, "require": { @@ -6260,7 +6275,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.21" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.25" }, "funding": [ { @@ -6271,25 +6286,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T13:27:38+00:00" + "time": "2025-08-20T06:48:20+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c" + "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/983ca05eec6623920d24ec0f1005f487d3734a0c", - "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", + "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", "shasum": "" }, "require": { @@ -6374,7 +6393,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.21" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.25" }, "funding": [ { @@ -6385,12 +6404,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-02T08:46:38+00:00" + "time": "2025-08-29T07:55:45+00:00" }, { "name": "symfony/intl", @@ -6880,16 +6903,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -6938,7 +6961,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -6949,16 +6972,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -7019,7 +7046,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -7030,6 +7057,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7039,7 +7070,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -7100,7 +7131,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -7111,6 +7142,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7120,16 +7155,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -7176,7 +7211,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -7187,25 +7222,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "000df7860439609837bbe28670b0be15783b7fbf" + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", - "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { @@ -7252,7 +7291,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -7263,12 +7302,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-20T12:04:08+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { "name": "symfony/polyfill-uuid", @@ -7351,16 +7394,16 @@ }, { "name": "symfony/process", - "version": "v6.4.20", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", + "url": "https://api.github.com/repos/symfony/process/zipball/6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", "shasum": "" }, "require": { @@ -7392,7 +7435,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.20" + "source": "https://github.com/symfony/process/tree/v6.4.25" }, "funding": [ { @@ -7403,12 +7446,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-10T17:11:00+00:00" + "time": "2025-08-14T06:23:17+00:00" }, { "name": "symfony/property-access", @@ -7575,16 +7622,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.18", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", "shasum": "" }, "require": { @@ -7638,7 +7685,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.18" + "source": "https://github.com/symfony/routing/tree/v6.4.24" }, "funding": [ { @@ -7649,25 +7696,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-01-09T08:51:02+00:00" + "time": "2025-07-15T08:46:37+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.14", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "4facd4174f45cd37c65860403412b67c7381136a" + "reference": "c1cc6721646f546627236c57f835272806087337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/4facd4174f45cd37c65860403412b67c7381136a", - "reference": "4facd4174f45cd37c65860403412b67c7381136a", + "url": "https://api.github.com/repos/symfony/runtime/zipball/c1cc6721646f546627236c57f835272806087337", + "reference": "c1cc6721646f546627236c57f835272806087337", "shasum": "" }, "require": { @@ -7717,7 +7768,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.14" + "source": "https://github.com/symfony/runtime/tree/v6.4.24" }, "funding": [ { @@ -7728,12 +7779,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-05T16:39:55+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/security-bundle", @@ -8189,16 +8244,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -8216,7 +8271,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -8252,7 +8307,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -8268,20 +8323,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.19", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", - "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898", "shasum": "" }, "require": { @@ -8314,7 +8369,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.24" }, "funding": [ { @@ -8325,25 +8380,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-21T10:06:30+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/string", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", + "url": "https://api.github.com/repos/symfony/string/zipball/7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", "shasum": "" }, "require": { @@ -8400,7 +8459,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.21" + "source": "https://github.com/symfony/string/tree/v6.4.25" }, "funding": [ { @@ -8411,12 +8470,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-18T15:23:29+00:00" + "time": "2025-08-22T12:33:20+00:00" }, { "name": "symfony/translation-contracts", @@ -8937,16 +9000,16 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6cd92486e9fc32506370822c57bc02353a5a92c", + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c", "shasum": "" }, "require": { @@ -8958,7 +9021,6 @@ "symfony/console": "<5.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^5.4|^6.0|^7.0", "symfony/error-handler": "^6.3|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", @@ -9002,7 +9064,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.25" }, "funding": [ { @@ -9013,25 +9075,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-09T07:34:50+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "717e7544aa99752c54ecba5c0e17459c48317472" + "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/717e7544aa99752c54ecba5c0e17459c48317472", - "reference": "717e7544aa99752c54ecba5c0e17459c48317472", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", + "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", "shasum": "" }, "require": { @@ -9079,7 +9145,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.21" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.25" }, "funding": [ { @@ -9090,12 +9156,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T21:06:26+00:00" + "time": "2025-08-18T13:06:32+00:00" }, { "name": "symfony/web-link", @@ -9182,16 +9252,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e" + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e54b060bc9c3dc3d4258bf0d165d0064e755f565", + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565", "shasum": "" }, "require": { @@ -9234,7 +9304,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.21" + "source": "https://github.com/symfony/yaml/tree/v6.4.25" }, "funding": [ { @@ -9245,12 +9315,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T09:48:44+00:00" + "time": "2025-08-26T16:59:00+00:00" }, { "name": "twig/twig", From 887dbf4295fbe3510a2f423f8295ef83902d3162 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 17:45:45 +0200 Subject: [PATCH 187/203] Small improvements on UsersPump --- src/Benzina/UsersPump.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index a1ff962..10f8d9f 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -59,20 +59,12 @@ class UsersPump implements PumpInterface $handle = UserService::asHandle($record['email']); } - $sequenceNumber = \substr($this->userCount, -2, 2); - - return \sprintf('%s_%s', $handle, $sequenceNumber); + return \sprintf('%s_%d', $handle, $this->userCount % 100); } private function getDateCreated(array $record): \DateTime { - $created = new \DateTime($record['created']); - - if ($created > new \DateTime('2011-01-01')) { - return $created; - } - - return new \DateTime($record['modified']); + return new \DateTime($record['created'] ?? $record['modified']); } private function getUserType(array $record): UserType From 6f5e9587f5a758935231997cef9e611213c9e0ba Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 19:20:21 +0200 Subject: [PATCH 188/203] Update localization pumping --- src/Benzina/LocalizedPumpTrait.php | 90 ++++++++++++++++++++++++++++++ src/Benzina/ProjectsPump.php | 25 ++++----- 2 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 src/Benzina/LocalizedPumpTrait.php diff --git a/src/Benzina/LocalizedPumpTrait.php b/src/Benzina/LocalizedPumpTrait.php new file mode 100644 index 0000000..7abdcf8 --- /dev/null +++ b/src/Benzina/LocalizedPumpTrait.php @@ -0,0 +1,90 @@ + */ + private static array $translatableFieldsCache = []; + + public function localize( + LocalizedEntityInterface $entity, + array $localizations = [], + array $context = [], + array $fieldCallbacks = [], + ): void { + if ($this->isDryRun($context)) { + return; + } + + $em = $this->getEntityManager(); + $meta = $em->getClassMetadata($entity::class); + $translations = $em->getRepository(Translation::class); + + $translatableFields = $this->getTranslatableFields($meta); + + foreach ($localizations as $localization) { + $locale = $localization['lang'] ?? null; + if (!$locale) { + continue; + } + + $entity->addLocale($locale); + + foreach ($translatableFields as $field) { + if (isset($fieldCallbacks[$field]) && is_callable($fieldCallbacks[$field])) { + $value = $fieldCallbacks[$field]($localization); + } else { + $value = $meta->getFieldValue($entity, $field); + } + + if ($value !== null) { + $translations->translate($entity, $field, $locale, $value); + } + } + } + + if ($this->preventFlushAndClear) { + return; + } + + $em->flush(); + $em->clear(); + } + + /** + * Get all #[Gedmo\Translatable] fields for this entity. + * + * @return string[] + */ + private function getTranslatableFields($meta): array + { + $className = $meta->getName(); + + if (isset(self::$translatableFieldsCache[$className])) { + return self::$translatableFieldsCache[$className]; + } + + $reflection = $meta->getReflectionClass(); + $fields = []; + + foreach ($reflection->getProperties() as $property) { + $attrs = $property->getAttributes(\Gedmo\Mapping\Annotation\Translatable::class); + if (!empty($attrs)) { + $fields[] = $property->getName(); + } + } + + self::$translatableFieldsCache[$className] = $fields; + + return $fields; + } +} diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 50e0720..ef1a5b5 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -16,7 +16,6 @@ use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; use App\Service\Project\TerritoryService; use Doctrine\Common\Collections\ArrayCollection; -use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -27,6 +26,7 @@ class ProjectsPump implements PumpInterface use DoctrinePumpTrait; use DatabasePumpTrait; use ProjectsPumpTrait; + use LocalizedPumpTrait; public function __construct( private ProjectRepository $projectRepository, @@ -83,24 +83,21 @@ class ProjectsPump implements PumpInterface $project->setDeadline($this->getProjectDeadline($conf)); $project->setCalendar($this->getProjectCalendar($record)); - $localizations = $this->getProjectLocalizations($project, $context); - $translations = $this->entityManager->getRepository(Translation::class); - foreach ($localizations as $localization) { - $locale = $localization['lang']; - - $project->addLocale($locale); - $translations - ->translate($project, 'title', $locale, $localization['name'] ?? $record['name']) - ->translate($project, 'subtitle', $locale, $localization['subtitle'] ?? $record['subtitle']) - ->translate($project, 'description', $locale, $this->getProjectDescription($localization)) - ; - } - + $project->addLocale($record['lang']); $project->setTitle($record['name']); $project->setSubtitle($record['subtitle']); $project->setDescription($this->getProjectDescription($record)); + $this->setPreventFlushAndClear(true); $this->persist($project, $context); + + $localizations = $this->getProjectLocalizations($project, $context); + + $this->setPreventFlushAndClear(false); + $this->localize($project, $localizations, $context, [ + 'title' => fn($l) => $l['name'], + 'description' => fn($l) => $this->getProjectDescription($l), + ]); } private function getProject(array $record): ?Project From 788a53fe22516821844d7b512d9ea4571d26fddd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 19:35:25 +0200 Subject: [PATCH 189/203] Update Reward entity to process claim changes count --- src/Entity/Project/Reward.php | 12 +++++++++--- src/State/Project/RewardClaimStateProcessor.php | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index 6428f21..b795e8c 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -58,19 +58,19 @@ class Reward implements LocalizedEntityInterface /** * For finite rewards, the total amount of existing unitsTotal. */ - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $unitsTotal = null; /** * The total amount of claimed rewards. */ - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $unitsClaimed = null; /** * For finite rewards, the currently available amount of unitsTotal that can be claimed. */ - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $unitsAvailable = null; /** @@ -198,6 +198,9 @@ class Reward implements LocalizedEntityInterface if (!$this->claims->contains($claim)) { $this->claims->add($claim); $claim->setReward($this); + + $this->unitsClaimed = $this->unitsClaimed + 1; + $this->unitsAvailable = $this->unitsAvailable - 1; } return $this; @@ -210,6 +213,9 @@ class Reward implements LocalizedEntityInterface if ($claim->getReward() === $this) { $claim->setReward(null); } + + $this->unitsClaimed = $this->unitsClaimed - 1; + $this->unitsAvailable = $this->unitsAvailable + 1; } return $this; diff --git a/src/State/Project/RewardClaimStateProcessor.php b/src/State/Project/RewardClaimStateProcessor.php index ea47d14..d133a86 100644 --- a/src/State/Project/RewardClaimStateProcessor.php +++ b/src/State/Project/RewardClaimStateProcessor.php @@ -37,6 +37,11 @@ class RewardClaimStateProcessor implements ProcessorInterface } $claim->setOwner($owner); + + $reward = $claim->getReward(); + $reward->addClaim($claim); + + $claim->setReward($reward); } $claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context); From ed2eb7c995c3ad32d17e2ab2071af91406ac8411 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 19:35:45 +0200 Subject: [PATCH 190/203] Update localized pumps --- src/Benzina/ProjectsBudgetPump.php | 22 +++++++++------------- src/Benzina/ProjectsRewardsPump.php | 25 ++++++++++--------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/Benzina/ProjectsBudgetPump.php b/src/Benzina/ProjectsBudgetPump.php index bdad575..5a819db 100644 --- a/src/Benzina/ProjectsBudgetPump.php +++ b/src/Benzina/ProjectsBudgetPump.php @@ -8,7 +8,6 @@ use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; use App\Repository\Project\ProjectRepository; -use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -18,6 +17,7 @@ class ProjectsBudgetPump implements PumpInterface use ArrayPumpTrait; use DatabasePumpTrait; use DoctrinePumpTrait; + use LocalizedPumpTrait; private const COST_KEYS = [ 'id', @@ -67,19 +67,15 @@ class ProjectsBudgetPump implements PumpInterface $budgetItem->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); $budgetItem->setDeadline($this->getDeadline($record)); - $localizations = $this->getCostLocalizations($budgetItem, $context); - $translations = $this->entityManager->getRepository(Translation::class); - foreach ($localizations as $localization) { - $locale = $localization['lang']; - - $budgetItem->addLocale($locale); - $translations - ->translate($budgetItem, 'title', $locale, $localization['cost'] ?? $record['cost']) - ->translate($budgetItem, 'description', $locale, $localization['description'] ?? $record['description']) - ; - } - + $this->setPreventFlushAndClear(true); $this->persist($budgetItem, $context); + + $localizations = $this->getCostLocalizations($budgetItem, $context); + + $this->setPreventFlushAndClear(false); + $this->localize($budgetItem, $localizations, $context, [ + 'title' => fn($l) => $l['cost'], + ]); } private function getProject(array $record): ?Project diff --git a/src/Benzina/ProjectsRewardsPump.php b/src/Benzina/ProjectsRewardsPump.php index 04795ac..aa197ea 100644 --- a/src/Benzina/ProjectsRewardsPump.php +++ b/src/Benzina/ProjectsRewardsPump.php @@ -7,7 +7,6 @@ use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\Reward; use App\Repository\Project\ProjectRepository; -use Gedmo\Translatable\Entity\Translation; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -17,6 +16,7 @@ class ProjectsRewardsPump implements PumpInterface use ArrayPumpTrait; use DatabasePumpTrait; use DoctrinePumpTrait; + use LocalizedPumpTrait; private const REWARD_KEYS = [ 'id', @@ -67,30 +67,25 @@ class ProjectsRewardsPump implements PumpInterface } $reward = new Reward(); - $reward->setTranslatableLocale($project->getLocales()[0]); + $reward->addLocale($project->getLocales()[0]); $reward->setProject($project); $reward->setMigrated(true); $reward->setMigratedId($record['id']); $reward->setTitle($record['reward']); $reward->setDescription($record['description'] ?? $record['reward']); $reward->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); - $reward->setHasUnits($record['units'] > 0); + $reward->setIsFinite($record['units'] > 0); $reward->setUnitsTotal($record['units'] ?? 0); - $reward->setUnitsAvailable($record['units'] ?? 0); + + $this->setPreventFlushAndClear(true); + $this->persist($reward, $context); $localizations = $this->getRewardLocalizations($reward, $context); - $translations = $this->entityManager->getRepository(Translation::class); - foreach ($localizations as $localization) { - $locale = $localization['lang']; - $reward->addLocale($locale); - $translations - ->translate($reward, 'title', $locale, $localization['reward'] ?? $record['reward']) - ->translate($reward, 'description', $locale, $localization['description'] ?? $record['description']) - ; - } - - $this->persist($reward, $context); + $this->setPreventFlushAndClear(false); + $this->localize($reward, $localizations, $context, [ + 'title' => fn($l) => $l['reward'], + ]); } private function getProject(array $record): ?Project From 69ad62f226e873c564564c3bcd4b918b4ff5a590 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 8 Sep 2025 20:10:08 +0200 Subject: [PATCH 191/203] Only update unitsAvailable on finite rewards --- src/ApiResource/Project/RewardApiResource.php | 2 +- src/Entity/Project/Reward.php | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index 4ea9ca9..3c864aa 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -74,7 +74,7 @@ class RewardApiResource public ?int $unitsTotal = null; /** - * The total amount of claimed units. + * The total amount of claims on this Reward. */ #[API\ApiProperty(writable: false)] #[API\ApiFilter(OrderFilter::class)] diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index b795e8c..13169e1 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -62,7 +62,7 @@ class Reward implements LocalizedEntityInterface private ?int $unitsTotal = null; /** - * The total amount of claimed rewards. + * The total amount of claims on this Reward. */ #[ORM\Column(nullable: true)] private ?int $unitsClaimed = null; @@ -76,7 +76,7 @@ class Reward implements LocalizedEntityInterface /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class)] + #[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class, cascade: ['persist'])] private Collection $claims; public function __construct() @@ -199,8 +199,7 @@ class Reward implements LocalizedEntityInterface $this->claims->add($claim); $claim->setReward($this); - $this->unitsClaimed = $this->unitsClaimed + 1; - $this->unitsAvailable = $this->unitsAvailable - 1; + $this->calcUnits(); } return $this; @@ -214,10 +213,18 @@ class Reward implements LocalizedEntityInterface $claim->setReward(null); } - $this->unitsClaimed = $this->unitsClaimed - 1; - $this->unitsAvailable = $this->unitsAvailable + 1; + $this->calcUnits(); } return $this; } + + private function calcUnits() + { + $this->unitsClaimed = \count($this->claims); + + if ($this->isFinite()) { + $this->unitsAvailable = $this->unitsTotal - $this->unitsClaimed; + } + } } From 2290dba170be8a0eeafecbca4ccd9bf6f505972b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 9 Sep 2025 14:18:03 +0200 Subject: [PATCH 192/203] Add dedicated pump for reward claims --- src/Benzina/InvestRewardsPump.php | 153 ++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/Benzina/InvestRewardsPump.php diff --git a/src/Benzina/InvestRewardsPump.php b/src/Benzina/InvestRewardsPump.php new file mode 100644 index 0000000..1cd96b0 --- /dev/null +++ b/src/Benzina/InvestRewardsPump.php @@ -0,0 +1,153 @@ + */ + private array $userCache = []; + + /** @var array */ + private array $chargeCache = []; + + /** @var array */ + private array $rewardCache = []; + + public function __construct( + private RewardRepository $rewardRepository, + private UserRepository $userRepository, + private ChargeRepository $chargeRepository, + private MoneyService $moneyService, + ) {} + + public function supports(mixed $sample): bool + { + if ($this->hasAllKeys($sample, ['invest', 'reward', 'fulfilled'])) { + return true; + } + + return false; + } + + public function pump(mixed $record, array $context): void + { + $reward = $this->getReward($record); + if ($reward === null) { + return; + } + + $invest = $this->getInvest($record, $context); + if ($invest === null) { + return; + } + + $charge = $this->getCharge($invest); + if ($charge === null) { + return; + } + + $user = $this->getUser($invest); + if ($user === null) { + return; + } + + if ($charge->getStatus() !== ChargeStatus::Charged) { + return; + } + + $claim = new RewardClaim(); + $claim->setOwner($user); + $claim->setCharge($charge); + + $reward->addClaim($claim); + $claim->setReward($reward); + + $this->persist($claim, $context); + } + + private function getInvest(array $record, array $context): ?array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest` i WHERE i.id = :invest' + ); + + $query->execute(['invest' => $record['invest']]); + + $result = $query->fetch(\PDO::FETCH_ASSOC); + + if (!$result) { + return null; + } + + return $result; + } + + private function getReward(array $record): ?Reward + { + $id = $record['reward']; + + if (isset($this->rewardCache[$id])) { + return $this->rewardRepository->find($this->rewardCache[$id]); + } + + $reward = $this->rewardRepository->findOneBy(['migratedId' => $id]); + if (!$reward) { + return null; + } + + $this->rewardCache[$id] = $reward->getId(); + + return $reward; + } + + private function getCharge(array $invest): ?Charge + { + $id = $invest['id']; + + if (isset($this->chargeCache[$id])) { + return $this->chargeRepository->find($this->chargeCache[$id]); + } + + $charge = $this->chargeRepository->findOneBy(['migratedId' => $id]); + if (!$charge) { + return null; + } + + $this->chargeCache[$id] = $charge->getId(); + + return $charge; + } + + private function getUser(array $invest): ?User + { + $id = $invest['user']; + + if (isset($this->userCache[$id])) { + return $this->userRepository->find($this->userCache[$id]); + } + + $user = $this->userRepository->findOneBy(['migratedId' => $id]); + + $this->userCache[$id] = $user->getId(); + + return $user; + } +} From 1129d776a441b03c921eaf8ac10b8a22837ed868 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 9 Sep 2025 14:18:51 +0200 Subject: [PATCH 193/203] Improve performance of InvestsPump via cache and micro optimizations --- src/Benzina/InvestsPump.php | 175 ++++++++++++++++++++++++++++-------- 1 file changed, 138 insertions(+), 37 deletions(-) diff --git a/src/Benzina/InvestsPump.php b/src/Benzina/InvestsPump.php index a3b6380..4c9ac5f 100644 --- a/src/Benzina/InvestsPump.php +++ b/src/Benzina/InvestsPump.php @@ -8,6 +8,7 @@ use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Gateway\Tracking; use App\Entity\Project\Project; +use App\Entity\Project\Reward; use App\Entity\Project\Support; use App\Entity\Tipjar; use App\Entity\User\User; @@ -23,6 +24,7 @@ use App\Gateway\Wallet\WalletGateway; use App\Money\Money; use App\Money\MoneyService; use App\Repository\Project\ProjectRepository; +use App\Repository\Project\RewardRepository; use App\Repository\Project\SupportRepository; use App\Repository\TipjarRepository; use App\Repository\User\UserRepository; @@ -53,11 +55,26 @@ class InvestsPump implements PumpInterface private const MAX_INT = 2147483647; private const CURRENCY = 'EUR'; + private ?int $tipjarCache = null; + + /** @var array */ + private array $userCache = []; + + /** @var array */ + private array $projectCache = []; + + /** @var array */ + private array $supportCache = []; + + /** @var array */ + private array $rewardCache = []; + public function __construct( private UserRepository $userRepository, private ProjectRepository $projectRepository, private SupportRepository $supportRepository, private TipjarRepository $tipjarRepository, + private RewardRepository $rewardRepository, private CheckoutService $checkoutService, private MoneyService $moneyService, ) {} @@ -92,13 +109,15 @@ class InvestsPump implements PumpInterface $project = $this->getProject($record); $tipjar = $this->getPlatformTipjar(); + + $now = new \DateTime(); $invested = new \DateTime($record['datetime'] ?? $record['invested']); $checkout = new Checkout(); $checkout->setMigrated(true); $checkout->setMigratedId($record['id']); $checkout->setDateCreated($invested); - $checkout->setDateUpdated(new \DateTime()); + $checkout->setDateUpdated($now); $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); @@ -111,9 +130,9 @@ class InvestsPump implements PumpInterface $charge = new Charge(); $charge->setMigrated(true); - $charge->getMigratedId($record['id']); + $charge->setMigratedId($record['id']); $charge->setDateCreated($invested); - $charge->setDateUpdated(new \DateTime()); + $charge->setDateUpdated($now); $charge->setStatus($this->getChargeStatus($record)); $charge->setType($this->getChargeType($record)); $charge->setMoney($this->getChargeMoney($record['amount'], self::CURRENCY)); @@ -133,7 +152,7 @@ class InvestsPump implements PumpInterface $tip->setMigrated(true); $tip->setMigratedId($record['id']); $tip->setDateCreated($invested); - $tip->setDateUpdated(new \DateTime()); + $tip->setDateUpdated($now); $tip->setStatus($this->getChargeStatus($record)); $tip->setType(ChargeType::Single); $tip->setTitle(self::CHARGE_TITLE_TIP); @@ -156,9 +175,12 @@ class InvestsPump implements PumpInterface $charge->addTransaction($transaction); - if ($project) { + if ( + $charge->getTarget()->getOwner() instanceof Project + && $charge->getStatus() === ChargeStatus::Charged + ) { $support = $this->getSupport($charge); - $support->setProject($project); + $support->setProject($charge->getTarget()->getOwner()); $support->setOrigin($checkout->getOrigin()); $support->addTransaction($transaction); $support->setMoney($this->calcSupportMoney($support)); @@ -173,7 +195,7 @@ class InvestsPump implements PumpInterface } $returned = new Transaction(); - $returned->setDateCreated(new \DateTime($record['returned'])); + $returned->setDateCreated(new \DateTime($record['returned'] ?? 'now')); $returned->setMoney($charge->getMoney()); $returned->setOrigin($charge->getTarget()); $returned->setTarget($checkout->getOrigin()); @@ -186,26 +208,87 @@ class InvestsPump implements PumpInterface private function getUser(array $record): ?User { - return $this->userRepository->findOneBy(['migratedId' => $record['user']]); + $id = $record['user']; + + if (isset($this->userCache[$id])) { + return $this->userRepository->find($this->userCache[$id]); + } + + $user = $this->userRepository->findOneBy(['migratedId' => $id]); + + $this->userCache[$id] = $user->getId(); + + return $user; } private function getProject(array $record): ?Project { - if (empty($record['project'])) { + $id = $record['project'] ?? null; + + if (!$id) { return null; } - return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); + if (isset($this->projectCache[$id])) { + return $this->projectRepository->find($id); + } + + $project = $this->projectRepository->findOneBy(['migratedId' => $id]); + + if (!$project) { + return null; + } + + $this->projectCache[$id] = $project->getId(); + + return $project; } - private function getSupport(Charge $charge): Support + private function getPlatformTipjar(): Tipjar { + if ($this->tipjarCache !== null) { + return $this->tipjarRepository->find($this->tipjarCache); + } + + $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); + + if ($tipjar) { + $this->tipjarCache = $tipjar->getId(); + + return $tipjar; + } + + $tipjar = new Tipjar(); + $tipjar->setName(self::PLATFORM_TIPJAR_NAME); + + $this->entityManager->persist($tipjar); + $this->entityManager->flush(); + + return $tipjar; + } + + private function getSupport(Charge $charge): ?Support + { + $origin = $charge->getCheckout()->getOrigin(); + $target = $charge->getTarget()->getOwner(); + + $originId = $origin->getId(); + $projectId = $target->getId(); + + $cacheKey = $projectId.'-'.$originId; + + if (isset($this->supportCache[$cacheKey])) { + return $this->supportRepository->find($this->supportCache[$cacheKey]); + } + $support = $this->supportRepository->findOneBy([ - 'origin' => $charge->getCheckout()->getOrigin()->getId(), - 'project' => $charge->getTarget()->getOwner()->getId(), + 'origin' => $origin, + 'project' => $target, ]); - if ($support) { + if ($support !== null) { + $this->supportCache[$cacheKey] = $support->getId(); + return $support; } @@ -243,35 +326,21 @@ class InvestsPump implements PumpInterface private function calcSupportMoney(Support $support): EmbeddableMoney { - $money = null; + $transactions = $support->getTransactions(); + if ($transactions->isEmpty()) { + return EmbeddableMoney::of(new Money(0, self::CURRENCY)); + } - foreach ($support->getTransactions() as $transaction) { - $money = $this->moneyService->add( - $transaction->getMoney(), - $money ?? new Money(0, $transaction->getMoney()->getCurrency()) - ); + $first = $transactions->first(); + $money = new Money(0, $first->getMoney()->getCurrency()); + + foreach ($transactions as $transaction) { + $money = $this->moneyService->add($transaction->getMoney(), $money); } return EmbeddableMoney::of($money); } - private function getPlatformTipjar(): Tipjar - { - $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); - - if ($tipjar) { - return $tipjar; - } - - $tipjar = new Tipjar(); - $tipjar->setName(self::PLATFORM_TIPJAR_NAME); - - $this->entityManager->persist($tipjar); - $this->entityManager->flush(); - - return $tipjar; - } - private function getInvestMsg(array $record, array $context) { $query = $this->getDbConnection($context)->prepare( @@ -398,4 +467,36 @@ class InvestsPump implements PumpInterface return $trackings; } + + private function getInvestReward(array $record, array $context) + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest_reward` ir WHERE ir.invest = :invest' + ); + + $query->execute(['invest' => $record['id']]); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + + private function getReward(array $record, array $context): ?Reward + { + $invested = $this->getInvestReward($record, $context); + + if (!$invested) { + return null; + } + + $id = $invested['reward']; + + if (isset($this->rewardCache[$id])) { + return $this->rewardRepository->find($this->rewardCache[$id]); + } + + $reward = $this->rewardRepository->findOneBy(['migratedId' => $invested['reward']]); + + $this->rewardCache[$id] = $reward->getId(); + + return $reward; + } } From 545f188c1e2cc1c3bacb03a22ea77e1a0570aef2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 9 Sep 2025 14:19:34 +0200 Subject: [PATCH 194/203] Add dummy cash gateway --- src/Gateway/Gateway/CashGateway.php | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Gateway/Gateway/CashGateway.php b/src/Gateway/Gateway/CashGateway.php index 7718b17..31ac495 100644 --- a/src/Gateway/Gateway/CashGateway.php +++ b/src/Gateway/Gateway/CashGateway.php @@ -2,12 +2,42 @@ namespace App\Gateway\Gateway; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\Checkout; use App\Gateway\GatewayInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -abstract class CashGateway implements GatewayInterface +class CashGateway implements GatewayInterface { public static function getName(): string { return 'cash'; } + + public static function getSupportedChargeTypes(): array + { + return []; + } + + public function process(Checkout $checkout): Checkout + { + return $checkout; + } + + public function handleRedirect(Request $request): RedirectResponse + { + return new Response(); + } + + public function handleWebhook(Request $request): Response + { + return new Response(); + } + + public function processRefund(Charge $charge): void + { + return; + } } From fe18ae8edc6ea11066cb440cdb3f656fd2c51cf3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 9 Sep 2025 14:19:45 +0200 Subject: [PATCH 195/203] Minor bug fixes --- src/ApiResource/ApiMoney.php | 2 +- src/ApiResource/Project/RewardApiResource.php | 2 ++ src/Benzina/UsersPump.php | 8 +++++++- src/Entity/Project/RewardClaim.php | 2 +- src/Entity/Project/Support.php | 2 +- src/EventListener/GatewayCheckoutSupportsListener.php | 4 ++-- src/Mapping/AutoMapper.php | 4 +++- src/Service/AccountingService.php | 5 +++-- 8 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/ApiResource/ApiMoney.php b/src/ApiResource/ApiMoney.php index 3d56faf..daab79e 100644 --- a/src/ApiResource/ApiMoney.php +++ b/src/ApiResource/ApiMoney.php @@ -28,7 +28,7 @@ class ApiMoney implements MoneyInterface * Conversion metadata. */ #[API\ApiProperty(readable: true, writable: false)] - public ?array $conversion; + public ?array $conversion = null; public function __construct( int $amount, diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index 3c864aa..8589018 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -13,6 +13,7 @@ use App\Entity\Project\Reward; use App\State\ApiResourceStateProvider; use App\State\Project\RewardStateProcessor; use AutoMapper\Attribute\MapTo; +use Symfony\Component\Serializer\Attribute\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; /** @@ -91,5 +92,6 @@ class RewardApiResource * @var RewardClaimApiResource[] */ #[API\ApiProperty(writable: false)] + #[MaxDepth(2)] public array $claims; } diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index 10f8d9f..53da200 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -64,7 +64,13 @@ class UsersPump implements PumpInterface private function getDateCreated(array $record): \DateTime { - return new \DateTime($record['created'] ?? $record['modified']); + $created = new \DateTime($record['created'] ?? '0000-00-00'); + + if ($created > new \DateTime('2011-01-01')) { + return $created; + } + + return new \DateTime($record['modified'] ?? 'now'); } private function getUserType(array $record): UserType diff --git a/src/Entity/Project/RewardClaim.php b/src/Entity/Project/RewardClaim.php index 1707ea5..023f39b 100644 --- a/src/Entity/Project/RewardClaim.php +++ b/src/Entity/Project/RewardClaim.php @@ -27,7 +27,7 @@ class RewardClaim implements UserOwnedInterface #[ORM\JoinColumn(nullable: false)] private ?Charge $charge = null; - #[ORM\ManyToOne(inversedBy: 'claims')] + #[ORM\ManyToOne(inversedBy: 'claims', cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] private ?Reward $reward = null; diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index 0262d3a..4a67613 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -16,7 +16,7 @@ use Doctrine\ORM\Mapping\Embedded; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'project_support')] -#[ORM\UniqueConstraint(fields: ['project', 'origin'])] +#[ORM\UniqueConstraint(fields: ['project', 'origin'], name: 'project_origin_unique')] #[ORM\Entity(repositoryClass: SupportRepository::class)] class Support { diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index 5960518..860685c 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -99,8 +99,8 @@ class GatewayCheckoutSupportsListener { /** @var Support|null */ $projectSupport = $this->supportRepository->findOneBy([ - 'project' => $project->getId(), - 'origin' => $origin->getId(), + 'project' => $project, + 'origin' => $origin, ]); if (!$projectSupport) { diff --git a/src/Mapping/AutoMapper.php b/src/Mapping/AutoMapper.php index 1e20c33..d63aedd 100644 --- a/src/Mapping/AutoMapper.php +++ b/src/Mapping/AutoMapper.php @@ -4,13 +4,15 @@ namespace App\Mapping; use AutoMapper\AutoMapper as InnerMapper; use AutoMapper\AutoMapperInterface; +use AutoMapper\MapperContext; class AutoMapper implements AutoMapperInterface { public const CACHE_DIR = 'automapper'; public const DEFAULT_CONTEXT = [ - 'skip_null_values' => true, + MapperContext::DEPTH => 1, + MapperContext::SKIP_NULL_VALUES => true, ]; private AutoMapperInterface $innerMapper; diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index e9c5380..09f7289 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -55,7 +55,7 @@ class AccountingService * * @return Transaction[] */ - private function getTransactionsInPeriod( + private function filterTransactions( array $transactions, \DateTimeInterface $start, \DateTimeInterface $end, @@ -97,13 +97,14 @@ class AccountingService $period->getStartDate(), $period->getEndDate(), ); + $points = []; $totalBalance = new Money(0, $accounting->getCurrency()); $accountingId = $accounting->getId(); foreach ($period as $start) { $end = \DateTime::createFromInterface($start)->add($period->getDateInterval()); - $periodTrxs = $this->getTransactionsInPeriod($trxs, $start, $end); + $periodTrxs = $this->filterTransactions($trxs, $start, $end); $balance = $aggregate ? $totalBalance : new Money(0, $accounting->getCurrency()); foreach ($periodTrxs as $trx) { From 62f42da09a647bcc69d77aae8c4a88da715714d0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 05:56:16 +0200 Subject: [PATCH 196/203] Update gateway api shape --- src/ApiResource/Gateway/ChargeApiResource.php | 4 +++- .../Gateway/CheckoutApiResource.php | 5 +++- src/Dto/Gateway/ChargeUpdationDto.php | 24 +++++-------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index ef8db2c..1b2ea13 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -86,13 +86,15 @@ class ChargeApiResource /** * The status of the charge item with the Gateway. */ - #[Assert\NotBlank()] + #[API\ApiProperty(writable: false)] #[API\ApiFilter(Filter\SearchFilter::class, strategy: 'exact')] public ChargeStatus $status = ChargeStatus::InPending; + #[API\ApiProperty(writable: false)] #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateCreated; + #[API\ApiProperty(writable: false)] #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateUpdated; } diff --git a/src/ApiResource/Gateway/CheckoutApiResource.php b/src/ApiResource/Gateway/CheckoutApiResource.php index 8f55637..7d3d3bc 100644 --- a/src/ApiResource/Gateway/CheckoutApiResource.php +++ b/src/ApiResource/Gateway/CheckoutApiResource.php @@ -60,7 +60,7 @@ class CheckoutApiResource * * @var ChargeApiResource[] */ - #[API\ApiProperty(readableLink: true, writableLink: true)] + #[API\ApiProperty(readableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] public array $charges = []; @@ -108,7 +108,10 @@ class CheckoutApiResource #[API\ApiProperty(writable: false)] public array $trackings = []; + #[API\ApiProperty(writable: false)] public \DateTimeInterface $dateCreated; + + #[API\ApiProperty(writable: false)] public \DateTimeInterface $dateUpdated; public static function parseLinks(array $values) diff --git a/src/Dto/Gateway/ChargeUpdationDto.php b/src/Dto/Gateway/ChargeUpdationDto.php index d42c745..9ec3656 100644 --- a/src/Dto/Gateway/ChargeUpdationDto.php +++ b/src/Dto/Gateway/ChargeUpdationDto.php @@ -2,33 +2,21 @@ namespace App\Dto\Gateway; +use ApiPlatform\Metadata as API; use App\Gateway\ChargeStatus; -use App\Gateway\ChargeType; +use Symfony\Component\Validator\Constraints as Assert; class ChargeUpdationDto { /** - * The unique identifier for the charge. + * The unique identifier for the GatewayCharge. */ + #[API\ApiProperty(writable: false)] public int $id; /** - * The type of the charge. - */ - public ChargeType $type; - - /** - * The title or name of the charge. - */ - public string $title; - - /** - * A detailed description of the charge. - */ - public string $description; - - /** - * The current payment status of the charge. + * To ask for a refund, set the status `to_refund`. */ + #[Assert\Choice([ChargeStatus::ToRefund])] public ChargeStatus $status; } From 73900633221168e9347d6b376706381a7a5714ec Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 06:29:58 +0200 Subject: [PATCH 197/203] Move CheckoutUpdationDto --- src/ApiResource/Gateway/CheckoutApiResource.php | 2 +- src/Dto/{ => Gateway}/CheckoutUpdationDto.php | 2 +- src/State/Gateway/CheckoutStateProcessor.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Dto/{ => Gateway}/CheckoutUpdationDto.php (94%) diff --git a/src/ApiResource/Gateway/CheckoutApiResource.php b/src/ApiResource/Gateway/CheckoutApiResource.php index 7d3d3bc..45c73b8 100644 --- a/src/ApiResource/Gateway/CheckoutApiResource.php +++ b/src/ApiResource/Gateway/CheckoutApiResource.php @@ -5,7 +5,7 @@ namespace App\ApiResource\Gateway; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; -use App\Dto\CheckoutUpdationDto; +use App\Dto\Gateway\CheckoutUpdationDto; use App\Entity\Gateway\Checkout; use App\Gateway\CheckoutStatus; use App\Gateway\Link; diff --git a/src/Dto/CheckoutUpdationDto.php b/src/Dto/Gateway/CheckoutUpdationDto.php similarity index 94% rename from src/Dto/CheckoutUpdationDto.php rename to src/Dto/Gateway/CheckoutUpdationDto.php index 02c3a13..6560f4a 100644 --- a/src/Dto/CheckoutUpdationDto.php +++ b/src/Dto/Gateway/CheckoutUpdationDto.php @@ -1,6 +1,6 @@ Date: Wed, 10 Sep 2025 06:40:22 +0200 Subject: [PATCH 198/203] Update custom ProjectReward validators --- .../AvailableRewardUnitsValidator.php | 4 +-- src/Validator/SameProjectRewardCharge.php | 16 ++++++++++ .../SameProjectRewardChargeValidator.php | 27 ++++++++++++++++ src/Validator/SingleRewardClaimPerCharge.php | 24 ++++++++++++++ .../SingleRewardClaimPerChargeValidator.php | 31 +++++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/Validator/SameProjectRewardCharge.php create mode 100644 src/Validator/SameProjectRewardChargeValidator.php create mode 100644 src/Validator/SingleRewardClaimPerCharge.php create mode 100644 src/Validator/SingleRewardClaimPerChargeValidator.php diff --git a/src/Validator/AvailableRewardUnitsValidator.php b/src/Validator/AvailableRewardUnitsValidator.php index bf51d40..e3949e8 100644 --- a/src/Validator/AvailableRewardUnitsValidator.php +++ b/src/Validator/AvailableRewardUnitsValidator.php @@ -13,11 +13,11 @@ final class AvailableRewardUnitsValidator extends ConstraintValidator */ public function validate(mixed $value, Constraint $constraint): void { - if (!$value->hasUnits) { + if (!$value->isFinite) { return; } - if ($value->hasUnits && $value->unitsAvailable > 0) { + if ($value->isFinite && $value->unitsAvailable > 0) { return; } diff --git a/src/Validator/SameProjectRewardCharge.php b/src/Validator/SameProjectRewardCharge.php new file mode 100644 index 0000000..38cf6dd --- /dev/null +++ b/src/Validator/SameProjectRewardCharge.php @@ -0,0 +1,16 @@ +charge; + $reward = $value->reward; + + if ($charge->target->project?->id === $reward->project->id) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} diff --git a/src/Validator/SingleRewardClaimPerCharge.php b/src/Validator/SingleRewardClaimPerCharge.php new file mode 100644 index 0000000..4b19130 --- /dev/null +++ b/src/Validator/SingleRewardClaimPerCharge.php @@ -0,0 +1,24 @@ +rewardClaimRepository->findOneBy([ + 'charge' => $value->charge->id, + ]); + + if ($claim === null) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} From 918d56b4c50f61aac1d4167348e0aff434063ede Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 06:40:47 +0200 Subject: [PATCH 199/203] Bugfixes on pumps --- src/Benzina/InvestsPump.php | 2 +- src/Benzina/ProjectsPump.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Benzina/InvestsPump.php b/src/Benzina/InvestsPump.php index 4c9ac5f..0f888cc 100644 --- a/src/Benzina/InvestsPump.php +++ b/src/Benzina/InvestsPump.php @@ -230,7 +230,7 @@ class InvestsPump implements PumpInterface } if (isset($this->projectCache[$id])) { - return $this->projectRepository->find($id); + return $this->projectRepository->find($this->projectCache[$id]); } $project = $this->projectRepository->findOneBy(['migratedId' => $id]); diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index ef1a5b5..3662011 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -153,8 +153,8 @@ class ProjectsPump implements PumpInterface { $calendar = new ProjectCalendar(); $calendar->release = new \DateTimeImmutable($record['published']); - $calendar->minimum = new \DateTimeImmutable($record['passed']); - $calendar->optimum = new \DateTimeImmutable($record['success']); + $calendar->minimum = new \DateTimeImmutable($record['passed'] ?? $record['closed']); + $calendar->optimum = new \DateTimeImmutable($record['success'] ?? $record['closed']); return $calendar; } From 613270ec00c987869908803b6e6a483f6e40e43d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 06:41:55 +0200 Subject: [PATCH 200/203] Update RewardClaim creation process --- .../Project/RewardClaimApiResource.php | 7 +++-- src/Dto/RewardClaimCreationDto.php | 30 +++++++++++++++++++ .../Project/RewardClaimStateProcessor.php | 5 ++-- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/Dto/RewardClaimCreationDto.php diff --git a/src/ApiResource/Project/RewardClaimApiResource.php b/src/ApiResource/Project/RewardClaimApiResource.php index 6ddfd97..704f5be 100644 --- a/src/ApiResource/Project/RewardClaimApiResource.php +++ b/src/ApiResource/Project/RewardClaimApiResource.php @@ -6,11 +6,11 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Gateway\ChargeApiResource; use App\ApiResource\User\UserApiResource; +use App\Dto\RewardClaimCreationDto; use App\Entity\Project\RewardClaim; use App\State\ApiResourceStateProvider; use App\State\Project\RewardClaimStateProcessor; use App\Validator\AvailableRewardUnits; -use App\Validator\EnoughRewardCharge; use Symfony\Component\Validator\Constraints as Assert; /** @@ -22,7 +22,10 @@ use Symfony\Component\Validator\Constraints as Assert; provider: ApiResourceStateProvider::class, processor: RewardClaimStateProcessor::class )] -#[EnoughRewardCharge()] +#[API\GetCollection()] +#[API\Post(input: RewardClaimCreationDto::class)] +#[API\Get()] +#[API\Delete()] class RewardClaimApiResource { #[API\ApiProperty(identifier: true, writable: false)] diff --git a/src/Dto/RewardClaimCreationDto.php b/src/Dto/RewardClaimCreationDto.php new file mode 100644 index 0000000..b6f5251 --- /dev/null +++ b/src/Dto/RewardClaimCreationDto.php @@ -0,0 +1,30 @@ +autoMapper->map($data, RewardClaim::class); - if (!$claim->getId()) { + if ($data instanceof RewardClaimCreationDto) { $owner = $this->authService->getUser(); if (!$owner) { From 3135f91f115034e189c8dfa037c0c8df448d4ebb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 06:45:40 +0200 Subject: [PATCH 201/203] Allow many to one on RewardClaim -> Charge --- src/Entity/Project/RewardClaim.php | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Entity/Project/RewardClaim.php b/src/Entity/Project/RewardClaim.php index 023f39b..a68c4f6 100644 --- a/src/Entity/Project/RewardClaim.php +++ b/src/Entity/Project/RewardClaim.php @@ -23,9 +23,9 @@ class RewardClaim implements UserOwnedInterface #[ORM\Column] private ?int $id = null; - #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] - private ?Charge $charge = null; + private ?User $owner = null; #[ORM\ManyToOne(inversedBy: 'claims', cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] @@ -33,25 +33,13 @@ class RewardClaim implements UserOwnedInterface #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] - private ?User $owner = null; + private ?Charge $charge = null; public function getId(): ?int { return $this->id; } - public function getCharge(): ?Charge - { - return $this->charge; - } - - public function setCharge(Charge $charge): static - { - $this->charge = $charge; - - return $this; - } - public function getReward(): ?Reward { return $this->reward; @@ -63,4 +51,16 @@ class RewardClaim implements UserOwnedInterface return $this; } + + public function getCharge(): ?Charge + { + return $this->charge; + } + + public function setCharge(?Charge $charge): static + { + $this->charge = $charge; + + return $this; + } } From 4dcc408e87f5ba8f4114f7fe8b70f4e3b9e62816 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 10 Sep 2025 07:00:14 +0200 Subject: [PATCH 202/203] Avoid overflow by split reward from claims in api output --- src/ApiResource/Project/RewardApiResource.php | 8 -------- src/ApiResource/Project/RewardClaimApiResource.php | 4 ++++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index 8589018..b2b1fca 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -13,7 +13,6 @@ use App\Entity\Project\Reward; use App\State\ApiResourceStateProvider; use App\State\Project\RewardStateProcessor; use AutoMapper\Attribute\MapTo; -use Symfony\Component\Serializer\Attribute\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; /** @@ -87,11 +86,4 @@ class RewardApiResource #[API\ApiProperty(writable: false)] #[API\ApiFilter(OrderFilter::class)] public int $unitsAvailable = 0; - - /** - * @var RewardClaimApiResource[] - */ - #[API\ApiProperty(writable: false)] - #[MaxDepth(2)] - public array $claims; } diff --git a/src/ApiResource/Project/RewardClaimApiResource.php b/src/ApiResource/Project/RewardClaimApiResource.php index 704f5be..1f644a0 100644 --- a/src/ApiResource/Project/RewardClaimApiResource.php +++ b/src/ApiResource/Project/RewardClaimApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource\Project; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Gateway\ChargeApiResource; @@ -35,12 +36,14 @@ class RewardClaimApiResource * The User claiming the ProjectReward. */ #[API\ApiProperty(writable: false)] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public UserApiResource $owner; /** * The GatewayCharge granting access to the ProjectReward. */ #[Assert\NotBlank()] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public ChargeApiResource $charge; /** @@ -48,5 +51,6 @@ class RewardClaimApiResource */ #[Assert\NotBlank()] #[AvailableRewardUnits()] + #[API\ApiFilter(SearchFilter::class, strategy: 'exact')] public RewardApiResource $reward; } From d2be2a012da2dd5aacc4f437893b713c3ab66011 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 11 Sep 2025 14:15:17 +0200 Subject: [PATCH 203/203] Update pumping code to adjust for 0916 mig --- src/Benzina/ProjectsBudgetPump.php | 15 ++++++++++++++- src/Benzina/ProjectsPump.php | 17 +++++++++++++---- src/Benzina/UsersPump.php | 2 +- src/Service/UserService.php | 4 ++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Benzina/ProjectsBudgetPump.php b/src/Benzina/ProjectsBudgetPump.php index 5a819db..746b442 100644 --- a/src/Benzina/ProjectsBudgetPump.php +++ b/src/Benzina/ProjectsBudgetPump.php @@ -19,6 +19,8 @@ class ProjectsBudgetPump implements PumpInterface use DoctrinePumpTrait; use LocalizedPumpTrait; + private const MAX_INT = 2147483647; + private const COST_KEYS = [ 'id', 'project', @@ -64,7 +66,7 @@ class ProjectsBudgetPump implements PumpInterface $budgetItem->setType($this->getCostType($record)); $budgetItem->setTitle($record['cost']); $budgetItem->setDescription($record['description'] ?? $record['cost']); - $budgetItem->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); + $budgetItem->setMoney($this->getCostMoney($record['amount'], 'EUR')); $budgetItem->setDeadline($this->getDeadline($record)); $this->setPreventFlushAndClear(true); @@ -114,4 +116,15 @@ class ProjectsBudgetPump implements PumpInterface return $query->fetchAll(); } + + private function getCostMoney(int $amount, string $currency): EmbeddableMoney + { + $amount = $amount * 100; + + if ($amount >= self::MAX_INT) { + $amount = self::MAX_INT; + } + + return new EmbeddableMoney($amount, $currency); + } } diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 3662011..db23ac3 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -51,7 +51,12 @@ class ProjectsPump implements PumpInterface } $status = $this->getProjectStatus($record); - if (\in_array($status, [ProjectStatus::InEditing, ProjectStatus::Rejected])) { + if (\in_array($status, [ProjectStatus::Rejected])) { + return; + } + + $created = new \DateTime($record['created']); + if (\in_array($status, [ProjectStatus::InDraft, ProjectStatus::InEditing]) && $created < new \DateTime('2024-01-01')) { return; } @@ -73,7 +78,7 @@ class ProjectsPump implements PumpInterface $project->setStatus($status); $project->setMigrated(true); $project->setMigratedId($record['id']); - $project->setDateCreated(new \DateTime($record['created'])); + $project->setDateCreated($created); $project->setDateUpdated(new \DateTime()); $project->setTranslatableLocale($record['lang']); $project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context))); @@ -84,8 +89,8 @@ class ProjectsPump implements PumpInterface $project->setCalendar($this->getProjectCalendar($record)); $project->addLocale($record['lang']); - $project->setTitle($record['name']); - $project->setSubtitle($record['subtitle']); + $project->setTitle($record['name'] ?? ''); + $project->setSubtitle($record['subtitle'] ?? ''); $project->setDescription($this->getProjectDescription($record)); $this->setPreventFlushAndClear(true); @@ -213,6 +218,10 @@ class ProjectsPump implements PumpInterface private function getProjectTerritory(array $record): Territory { + if ($record['project_location'] === null) { + return Territory::unknown(); + } + $cleanAddress = $this->cleanProjectLocation($record['project_location'], 2); if ($cleanAddress === '') { diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index 53da200..f7b0892 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -59,7 +59,7 @@ class UsersPump implements PumpInterface $handle = UserService::asHandle($record['email']); } - return \sprintf('%s_%d', $handle, $this->userCount % 100); + return \sprintf('%s_%02d', $handle, $this->userCount % 100); } private function getDateCreated(array $record): \DateTime diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 84633f7..f9f2044 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -18,8 +18,8 @@ class UserService $value = $matches[0]; } - // Only lowercase a-z, numbers and underscore in user handles - $value = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($value)); + // Only lowercase a-z, numbers, underscore and middle dots in user handles + $value = \preg_replace('/[^a-z0-9_.]|^\.|\.$/', '_', \strtolower($value)); // Min length 4 $value = \str_pad($value, 4, '_');