From 6e95ac42c97f640f2a45b53d97abe4824435d8df Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 11:27:04 +0200 Subject: [PATCH 01/86] Allow single line on empty function body --- .php-cs-fixer.dist.php | 1 + src/ApiResource/Gateway.php | 4 +--- src/ApiResource/Version.php | 3 +-- src/Command/SystemVarsListCommand.php | 4 +--- src/Controller/GatewaysController.php | 3 +-- src/DependencyInjection/SystemVariablesLoader.php | 3 +-- src/Library/Benzina/Pump/CheckoutsPump.php | 3 +-- src/Library/Benzina/Pump/ProjectsPump.php | 3 +-- src/Library/Benzina/Pump/UsersPump.php | 3 +-- src/Library/Economy/Payment/PaypalGateway.php | 3 +-- src/OpenApi/OpenApiFactory.php | 4 +--- src/Security/AccessTokenHandler.php | 4 +--- src/Service/Auth/AuthService.php | 3 +-- src/Service/GatewayCheckoutService.php | 3 +-- src/State/GatewayCheckoutProcessor.php | 3 +-- src/State/GatewayStateProvider.php | 3 +-- src/State/UserStateProcessor.php | 3 +-- src/State/UserTokenLoginProcessor.php | 3 +-- src/Validator/GatewayNameValidator.php | 3 +-- 19 files changed, 19 insertions(+), 40 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b8c750c..97cb0b0 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,6 +8,7 @@ return (new PhpCsFixer\Config()) ->setRules([ '@Symfony' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'single_line_empty_body' => true, 'yoda_style' => [ 'equal' => false, 'identical' => false, diff --git a/src/ApiResource/Gateway.php b/src/ApiResource/Gateway.php index a143a64..f5917f8 100644 --- a/src/ApiResource/Gateway.php +++ b/src/ApiResource/Gateway.php @@ -17,9 +17,7 @@ use App\State\GatewayStateProvider; #[API\Get(provider: GatewayStateProvider::class)] class Gateway { - public function __construct(private readonly GatewayInterface $gateway) - { - } + public function __construct(private readonly GatewayInterface $gateway) {} #[API\ApiProperty(identifier: true)] public function getName(): string diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php index b94e121..71e6437 100644 --- a/src/ApiResource/Version.php +++ b/src/ApiResource/Version.php @@ -24,8 +24,7 @@ class Version public function __construct( private readonly LogEntry $log, private readonly object $entity, - ) { - } + ) {} /** * The ID of the version record. diff --git a/src/Command/SystemVarsListCommand.php b/src/Command/SystemVarsListCommand.php index 1be82e6..06a805b 100644 --- a/src/Command/SystemVarsListCommand.php +++ b/src/Command/SystemVarsListCommand.php @@ -21,9 +21,7 @@ class SystemVarsListCommand extends Command parent::__construct(); } - protected function configure(): void - { - } + protected function configure(): void {} protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Controller/GatewaysController.php b/src/Controller/GatewaysController.php index 1c65c4e..4640201 100644 --- a/src/Controller/GatewaysController.php +++ b/src/Controller/GatewaysController.php @@ -19,8 +19,7 @@ class GatewaysController extends AbstractController public function __construct( private GatewayLocator $gatewayLocator, - ) { - } + ) {} #[Route('/gateway_redirects', name: self::REDIRECT)] public function handleRedirect(Request $request): Response diff --git a/src/DependencyInjection/SystemVariablesLoader.php b/src/DependencyInjection/SystemVariablesLoader.php index a877679..0db83d9 100644 --- a/src/DependencyInjection/SystemVariablesLoader.php +++ b/src/DependencyInjection/SystemVariablesLoader.php @@ -15,8 +15,7 @@ final class SystemVariablesLoader implements EnvVarLoaderInterface { public function __construct( private SystemVariableRepository $systemVarRepository, - ) { - } + ) {} public function loadEnvVars(): array { diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 3d50f4d..b1fa028 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -76,8 +76,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private TipjarRepository $tipjarRepository, private EntityManagerInterface $entityManager, private GatewayCheckoutService $checkoutService, - ) { - } + ) {} public function supports(mixed $batch): bool { diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index a93870c..cb31167 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -97,8 +97,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface public function __construct( private UserRepository $userRepository, private EntityManagerInterface $entityManager, - ) { - } + ) {} public function supports(mixed $batch): bool { diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 14485b5..9e42a6b 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -52,8 +52,7 @@ class UsersPump extends AbstractPump implements PumpInterface public function __construct( private EntityManagerInterface $entityManager, - ) { - } + ) {} public function supports(mixed $batch): bool { diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 22d1236..7f2e4da 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -49,8 +49,7 @@ class PaypalGateway implements GatewayInterface private IriConverterInterface $iriConverter, private GatewayCheckoutService $checkoutService, private GatewayCheckoutRepository $checkoutRepository, - ) { - } + ) {} public static function getName(): string { diff --git a/src/OpenApi/OpenApiFactory.php b/src/OpenApi/OpenApiFactory.php index 1ff77c1..f001e75 100644 --- a/src/OpenApi/OpenApiFactory.php +++ b/src/OpenApi/OpenApiFactory.php @@ -12,9 +12,7 @@ use ApiPlatform\OpenApi\OpenApi; class OpenApiFactory implements OpenApiFactoryInterface { - public function __construct(private OpenApiFactoryInterface $decorated) - { - } + public function __construct(private OpenApiFactoryInterface $decorated) {} private function updateOperation(Operation $operation): Operation { diff --git a/src/Security/AccessTokenHandler.php b/src/Security/AccessTokenHandler.php index bc9488c..96f0197 100644 --- a/src/Security/AccessTokenHandler.php +++ b/src/Security/AccessTokenHandler.php @@ -9,9 +9,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface { - public function __construct(private UserTokenRepository $userTokenRepository) - { - } + public function __construct(private UserTokenRepository $userTokenRepository) {} public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge { diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 5d3795f..d9e7a40 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -13,8 +13,7 @@ class AuthService public function __construct( private string $appSecret, - ) { - } + ) {} /** * @return array{CORS_ALLOW_ORIGIN: string, SESSION_LIFETIME: int} diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 54d049b..7939bb7 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -16,8 +16,7 @@ class GatewayCheckoutService public function __construct( private RouterInterface $router, - ) { - } + ) {} /** * Generates a suitable value for redirection params to external gateways. diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index 5935fe9..f8b02c0 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -16,8 +16,7 @@ class GatewayCheckoutProcessor implements ProcessorInterface private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, private EntityManagerInterface $entityManager, - ) { - } + ) {} /** * @param GatewayCheckout $data diff --git a/src/State/GatewayStateProvider.php b/src/State/GatewayStateProvider.php index 0ae771b..c9130b4 100644 --- a/src/State/GatewayStateProvider.php +++ b/src/State/GatewayStateProvider.php @@ -13,8 +13,7 @@ class GatewayStateProvider implements ProviderInterface { public function __construct( private GatewayLocator $gateways, - ) { - } + ) {} private function getGateways(): array { diff --git a/src/State/UserStateProcessor.php b/src/State/UserStateProcessor.php index 5632ec3..f9d1452 100644 --- a/src/State/UserStateProcessor.php +++ b/src/State/UserStateProcessor.php @@ -14,8 +14,7 @@ class UserStateProcessor implements ProcessorInterface public function __construct( private ProcessorInterface $innerProcessor, private UserPasswordHasherInterface $userPasswordHasher, - ) { - } + ) {} /** * @return T2 diff --git a/src/State/UserTokenLoginProcessor.php b/src/State/UserTokenLoginProcessor.php index 3c55e44..b3aa54b 100644 --- a/src/State/UserTokenLoginProcessor.php +++ b/src/State/UserTokenLoginProcessor.php @@ -21,8 +21,7 @@ class UserTokenLoginProcessor implements ProcessorInterface private UserRepository $userRepository, private EntityManagerInterface $entityManager, private UserPasswordHasherInterface $userPasswordHasher, - ) { - } + ) {} /** * @param UserTokenLoginDto $data diff --git a/src/Validator/GatewayNameValidator.php b/src/Validator/GatewayNameValidator.php index 4dfe265..7519616 100644 --- a/src/Validator/GatewayNameValidator.php +++ b/src/Validator/GatewayNameValidator.php @@ -11,8 +11,7 @@ class GatewayNameValidator extends ChoiceValidator { public function __construct( private GatewayLocator $gateways, - ) { - } + ) {} public function validate($value, Constraint $constraint): void { From 14113ff564488eea65272ae7daa1eeb07213c5b7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 11:28:40 +0200 Subject: [PATCH 02/86] Rename Gateway api resource to GatewayApiResource --- src/ApiResource/{Gateway.php => GatewayApiResource.php} | 3 ++- src/State/GatewayStateProvider.php | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) rename src/ApiResource/{Gateway.php => GatewayApiResource.php} (93%) diff --git a/src/ApiResource/Gateway.php b/src/ApiResource/GatewayApiResource.php similarity index 93% rename from src/ApiResource/Gateway.php rename to src/ApiResource/GatewayApiResource.php index f5917f8..e8eea9e 100644 --- a/src/ApiResource/Gateway.php +++ b/src/ApiResource/GatewayApiResource.php @@ -13,9 +13,10 @@ use App\State\GatewayStateProvider; * These implementations make use of external or internal services to gather the funds that are inside a Transaction, * perform corroboration of funds and store the Transactions into the system. */ +#[API\ApiResource(shortName: 'Gateway')] #[API\GetCollection(provider: GatewayStateProvider::class)] #[API\Get(provider: GatewayStateProvider::class)] -class Gateway +class GatewayApiResource { public function __construct(private readonly GatewayInterface $gateway) {} diff --git a/src/State/GatewayStateProvider.php b/src/State/GatewayStateProvider.php index c9130b4..f88f1a8 100644 --- a/src/State/GatewayStateProvider.php +++ b/src/State/GatewayStateProvider.php @@ -5,7 +5,7 @@ namespace App\State; use ApiPlatform\Metadata as API; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; -use App\ApiResource\Gateway; +use App\ApiResource\GatewayApiResource; use App\Library\Economy\Payment\GatewayLocator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -19,18 +19,18 @@ class GatewayStateProvider implements ProviderInterface { $gateways = []; foreach ($this->gateways->getGateways() as $gateway) { - $gateways[] = new Gateway($gateway); + $gateways[] = new GatewayApiResource($gateway); } return $gateways; } - private function getGateway(string $name): Gateway + private function getGateway(string $name): GatewayApiResource { try { $gateway = $this->gateways->getGateway($name); - return new Gateway($gateway); + return new GatewayApiResource($gateway); } catch (\Exception $e) { throw new NotFoundHttpException('Not Found'); } From 7aa60ffce60a439d038e758c47afa76ec918f6d9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 11:58:13 +0200 Subject: [PATCH 03/86] Update paypal and stripe link and tracking usage --- src/Library/Economy/Payment/PaypalGateway.php | 22 +++++++++---------- src/Library/Economy/Payment/StripeGateway.php | 12 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 7f2e4da..ebea710 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -65,8 +65,8 @@ class PaypalGateway implements GatewayInterface ]); $tracking = new GatewayTracking(); - $tracking->setValue($order['id']); - $tracking->setTitle(self::TRACKING_TITLE_ORDER); + $tracking->title = self::TRACKING_TITLE_ORDER; + $tracking->value = $order['id']; $checkout->addGatewayTracking($tracking); @@ -76,10 +76,10 @@ class PaypalGateway implements GatewayInterface : GatewayLinkType::Debug; $link = new GatewayLink(); - $link->setHref($linkData['href']); - $link->setRel($linkData['rel']); - $link->setMethod($linkData['method']); - $link->setType($linkType); + $link->href = $linkData['href']; + $link->rel = $linkData['rel']; + $link->method = $linkData['method']; + $link->type = $linkType; $checkout->addGatewayLink($link); } @@ -121,8 +121,8 @@ class PaypalGateway implements GatewayInterface foreach ($capture['purchase_units'] as $purchaseUnit) { $tracking = new GatewayTracking(); - $tracking->setValue($purchaseUnit['payments']['captures'][0]['id']); - $tracking->setTitle(self::TRACKING_TITLE_TRANSACTION); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; $checkout->addGatewayTracking($tracking); } @@ -168,8 +168,8 @@ class PaypalGateway implements GatewayInterface foreach ($event['resource']['purchase_units'] as $purchaseUnit) { $tracking = new GatewayTracking(); - $tracking->setValue($purchaseUnit['payments']['captures'][0]['id']); - $tracking->setTitle(self::TRACKING_TITLE_TRANSACTION); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; $checkout->addGatewayTracking($tracking); } @@ -196,7 +196,7 @@ class PaypalGateway implements GatewayInterface foreach ($checkout->getCharges() as $charge) { $money = $this->getPaypalMoney($charge); - $reference = $this->checkoutService->getGatewayReference($checkout, $charge); + $reference = $this->checkoutService->generateTracking($checkout, $charge); $units[] = [ 'reference_id' => $reference, diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 0874cbe..568ce43 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -59,16 +59,16 @@ class StripeGateway implements GatewayInterface $link = new GatewayLink(); - $link->setHref($session->url); - $link->setRel('approve'); - $link->setMethod(Request::METHOD_GET); - $link->setType(GatewayLinkType::Payment); + $link->href = $session->url; + $link->rel = 'approve'; + $link->method = Request::METHOD_GET; + $link->type = GatewayLinkType::Payment; $checkout->addGatewayLink($link); $tracking = new GatewayTracking(); - $tracking->setValue($session->id); - $tracking->setTitle(self::TRACKING_TITLE_CHECKOUT); + $tracking->title = self::TRACKING_TITLE_CHECKOUT; + $tracking->value = $session->id; $checkout->addGatewayTracking($tracking); From d3c9bd9b80a2dadd3a3d7d9f80bb403dd3b8fe2a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 13:51:47 +0200 Subject: [PATCH 04/86] Ensure transactions update origin and target accountings --- .../AccountingTransactionsListener.php | 34 ++++++++++++++ tests/Entity/AccountingTest.php | 47 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/EventListener/AccountingTransactionsListener.php diff --git a/src/EventListener/AccountingTransactionsListener.php b/src/EventListener/AccountingTransactionsListener.php new file mode 100644 index 0000000..48c309c --- /dev/null +++ b/src/EventListener/AccountingTransactionsListener.php @@ -0,0 +1,34 @@ +getOrigin(); + $origin->addTransactionsIssued($transaction); + + $this->entityManager->persist($origin); + + $target = $transaction->getTarget(); + $target->addTransactionsReceived($transaction); + + $this->entityManager->persist($target); + + $this->entityManager->flush(); + } +} diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php index df69e3b..29fde8c 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/AccountingTest.php @@ -3,6 +3,8 @@ namespace App\Tests\Entity; use App\Entity\Accounting; +use App\Entity\AccountingTransaction; +use App\Entity\Money; use App\Entity\Tipjar; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -59,4 +61,49 @@ class AccountingTest extends KernelTestCase $this->assertEquals(Tipjar::class, $accountingB->getOwnerClass()); $this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName()); } + + public function testAccountingGetsUpdatedByTransactions() + { + $tipjarA = new Tipjar(); + $tipjarA->setName('TEST_TIPJAR_A'); + + $accountingA = new Accounting(); + $accountingA->setTipjar($tipjarA); + + $tipjarB = new Tipjar(); + $tipjarB->setName('TEST_TIPJAR_B'); + + $accountingB = new Accounting(); + $accountingB->setTipjar($tipjarB); + + $transaction = new AccountingTransaction(); + $transaction->setMoney(new Money(120, 'EUR')); + $transaction->setOrigin($accountingA); + $transaction->setTarget($accountingB); + + $this->entityManager->persist($transaction); + $this->entityManager->flush(); + + $accountingA = $this->entityManager + ->getRepository(Tipjar::class) + ->findOneBy(['name' => 'TEST_TIPJAR_A']) + ->getAccounting(); + + $accountingB = $this->entityManager + ->getRepository(Tipjar::class) + ->findOneBy(['name' => 'TEST_TIPJAR_B']) + ->getAccounting(); + + $this->assertCount(1, $accountingA->getTransactionsIssued()); + $this->assertCount(0, $accountingA->getTransactionsReceived()); + + $this->assertCount(0, $accountingB->getTransactionsIssued()); + $this->assertCount(1, $accountingB->getTransactionsReceived()); + + $this->assertEquals(120, $accountingA->getTransactionsIssued()[0]->getMoney()->amount); + $this->assertEquals('EUR', $accountingA->getTransactionsIssued()[0]->getMoney()->currency); + + $this->assertEquals(120, $accountingB->getTransactionsReceived()[0]->getMoney()->amount); + $this->assertEquals('EUR', $accountingB->getTransactionsReceived()[0]->getMoney()->currency); + } } From 3850eb00c9c8fe7430413fd9f60f966064f0e25f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 13:52:17 +0200 Subject: [PATCH 05/86] Fix cache issues in PaypalGatewayService --- src/Library/Economy/Payment/PaypalGatewayService.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGatewayService.php b/src/Library/Economy/Payment/PaypalGatewayService.php index 7b04d99..1c77f9c 100644 --- a/src/Library/Economy/Payment/PaypalGatewayService.php +++ b/src/Library/Economy/Payment/PaypalGatewayService.php @@ -21,7 +21,11 @@ class PaypalGatewayService private string $paypalWebhookId, private HttpClientInterface $httpClient, ) { - $this->cache = new FilesystemAdapter(); + $this->cache = new FilesystemAdapter(\preg_replace( + '/[^-+_\.A-Za-z0-9]/', + '-', + $this->paypalApiAddress + )); $httpOptions = new HttpOptions(); $httpOptions->setBaseUri($paypalApiAddress); @@ -58,7 +62,7 @@ class PaypalGatewayService */ public function getAuthToken(): array { - return $this->cache->get($this->paypalApiAddress, function (ItemInterface $item): array { + return $this->cache->get(urlencode('/v1/oauth2/token'), function (ItemInterface $item): array { $tokenData = $this->generateAuthToken(); $item->expiresAfter($tokenData['expires_in']); From a888c3447a4c4de114073ce4b34d7dd876aa31ef Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 13:53:31 +0200 Subject: [PATCH 06/86] Persist checkout after gateway handling --- src/Library/Economy/Payment/PaypalGateway.php | 3 +++ src/Library/Economy/Payment/StripeGateway.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index ebea710..d36267e 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -129,6 +129,9 @@ class PaypalGateway implements GatewayInterface $checkout = $this->checkoutService->chargeCheckout($checkout); + $this->entityManager->persist($checkout); + $this->entityManager->flush(); + // TO-DO: This should redirect the user to a GUI return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 568ce43..5fd73bf 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -100,6 +100,9 @@ class StripeGateway implements GatewayInterface $checkout = $this->checkoutService->chargeCheckout($checkout); + $this->entityManager->persist($checkout); + $this->entityManager->flush(); + // TO-DO: This should redirect the user to a GUI return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } From 8c9ee0d215e5f82fab31f6a4a150707a80ccab34 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 12:25:42 +0200 Subject: [PATCH 07/86] Make Gateways specify the charge types they support --- src/ApiResource/GatewayApiResource.php | 10 ++++++++++ src/Library/Economy/Payment/GatewayInterface.php | 5 +++++ src/Library/Economy/Payment/PaypalGateway.php | 8 ++++++++ src/Library/Economy/Payment/StripeGateway.php | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/src/ApiResource/GatewayApiResource.php b/src/ApiResource/GatewayApiResource.php index e8eea9e..d93ac22 100644 --- a/src/ApiResource/GatewayApiResource.php +++ b/src/ApiResource/GatewayApiResource.php @@ -25,4 +25,14 @@ class GatewayApiResource { return $this->gateway->getName(); } + + /** + * The GatewayCharge types that can be processed by this Gateway. + * + * @return \App\Entity\GatewayChargeType[] + */ + public function getSupported(): array + { + return $this->gateway->getSupportedChargeTypes(); + } } diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index c9cb0bc..0f41ffc 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -28,6 +28,11 @@ interface GatewayInterface */ public static function getName(): string; + /** + * @return \App\Entity\GatewayChargeType[] The charge types that can be processed by this Gateway + */ + public static function getSupportedChargeTypes(): array; + /** * Connects with the payment gateway and creates a checkout session so the gateway can process the payment. * diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index d36267e..d574fc8 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -4,6 +4,7 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; use App\Entity\GatewayCharge; +use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; use App\Entity\GatewayCheckoutStatus; use App\Entity\GatewayLink; @@ -56,6 +57,13 @@ class PaypalGateway implements GatewayInterface return 'paypal'; } + public static function getSupportedChargeTypes(): array + { + return [ + GatewayChargeType::Single, + ]; + } + public function process(GatewayCheckout $checkout): GatewayCheckout { $order = $this->paypal->postOrder([ diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 5fd73bf..48a4e74 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -44,6 +44,14 @@ class StripeGateway implements GatewayInterface return 'stripe'; } + public static function getSupportedChargeTypes(): array + { + return [ + GatewayChargeType::Single, + GatewayChargeType::Recurring, + ]; + } + public function process(GatewayCheckout $checkout): GatewayCheckout { $session = $this->stripe->checkout->sessions->create([ From e568502d06ea4c9537a67234929cf36683765e9f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 12:26:12 +0200 Subject: [PATCH 08/86] Allow easier injection of future dependencies --- src/EventListener/AccountingTransactionsListener.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EventListener/AccountingTransactionsListener.php b/src/EventListener/AccountingTransactionsListener.php index 48c309c..423a275 100644 --- a/src/EventListener/AccountingTransactionsListener.php +++ b/src/EventListener/AccountingTransactionsListener.php @@ -12,12 +12,12 @@ use Doctrine\ORM\Events; final class AccountingTransactionsListener { public function __construct( - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) {} public function postPersist( AccountingTransaction $transaction, - PostPersistEventArgs $event + PostPersistEventArgs $event, ) { $origin = $transaction->getOrigin(); $origin->addTransactionsIssued($transaction); From 757e27ed24ab331cdfe1e5f1afa16043ca12b781 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 13:55:47 +0200 Subject: [PATCH 09/86] Update checkout processor return --- src/State/GatewayCheckoutProcessor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index f8b02c0..93e4de8 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -6,7 +6,6 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Entity\GatewayCheckout; use App\Library\Economy\Payment\GatewayLocator; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; class GatewayCheckoutProcessor implements ProcessorInterface @@ -15,11 +14,12 @@ class GatewayCheckoutProcessor implements ProcessorInterface #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, - private EntityManagerInterface $entityManager, ) {} /** * @param GatewayCheckout $data + * + * @return GatewayCheckout */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { @@ -28,6 +28,6 @@ class GatewayCheckoutProcessor implements ProcessorInterface $gateway = $this->gatewayLocator->getGatewayOf($checkout); $checkout = $gateway->process($checkout); - return $this->innerProcessor->process($data, $operation, $uriVariables, $context); + return $this->innerProcessor->process($checkout, $operation, $uriVariables, $context); } } From 05890d08f15c5f89caa8a9febc7dd2f60c6d41d3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 14:04:12 +0200 Subject: [PATCH 10/86] Update docblocks --- src/DependencyInjection/Compiler/GatewaysCompilerPass.php | 4 +--- src/Library/Economy/Payment/GatewayLocator.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php index ad0d23b..0bb40f5 100755 --- a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php +++ b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php @@ -104,9 +104,7 @@ class GatewaysCompilerPass implements CompilerPassInterface * * @param array $gatewayClasses Fully-qualified Gateway class names * - * @throws \Exception If there are two different Gateway classes with the same name - * - * @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations + * @throws \Exception If there are two different Gateway classes that return the same name string */ public static function validateGatewayNames(array $gatewayClasses): void { diff --git a/src/Library/Economy/Payment/GatewayLocator.php b/src/Library/Economy/Payment/GatewayLocator.php index 5c80a56..ad57251 100644 --- a/src/Library/Economy/Payment/GatewayLocator.php +++ b/src/Library/Economy/Payment/GatewayLocator.php @@ -45,7 +45,7 @@ class GatewayLocator /** * @return array List of the available Gateway names * - * @see GatewaysCompilerPass::compileGatewayNames() + * @see \App\DependencyInjection\Compiler\GatewaysCompilerPass::compileGatewayNames() */ public static function getNamesStatic(): array { From fec131b9974e6b1cfd499e1131e4523836ef7fca Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 14:04:24 +0200 Subject: [PATCH 11/86] Improve GatewayName validation --- src/Validator/GatewayName.php | 3 ++- src/Validator/GatewayNameValidator.php | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Validator/GatewayName.php b/src/Validator/GatewayName.php index 3772634..b3350ce 100644 --- a/src/Validator/GatewayName.php +++ b/src/Validator/GatewayName.php @@ -15,7 +15,8 @@ class GatewayName extends Choice public function __construct() { parent::__construct( - options: GatewayLocator::getNamesStatic() + options: GatewayLocator::getNamesStatic(), + message: 'Available Gateways are {{ choices }}.' ); } } diff --git a/src/Validator/GatewayNameValidator.php b/src/Validator/GatewayNameValidator.php index 7519616..08393e0 100644 --- a/src/Validator/GatewayNameValidator.php +++ b/src/Validator/GatewayNameValidator.php @@ -22,9 +22,9 @@ class GatewayNameValidator extends ChoiceValidator } try { - if ($this->gateways->getGateway($value)) { - return; - } + $this->gateways->getGateway($value); + + return; } catch (\Exception $e) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) From dc6eb18a73141ce43e90e22520c4a0411a6a08e3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 15:17:54 +0200 Subject: [PATCH 12/86] Add wallet statement entity --- src/Entity/WalletStatement.php | 129 +++++++++++++++++++ src/Entity/WalletStatementDirection.php | 16 +++ src/Repository/WalletStatementRepository.php | 75 +++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/Entity/WalletStatement.php create mode 100644 src/Entity/WalletStatementDirection.php create mode 100644 src/Repository/WalletStatementRepository.php diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php new file mode 100644 index 0000000..24f2e4f --- /dev/null +++ b/src/Entity/WalletStatement.php @@ -0,0 +1,129 @@ + + */ + #[ORM\ManyToMany(targetEntity: WalletStatement::class, mappedBy: 'financesTo')] + private Collection $financedBy; + + /** + * The Statements that drawed money out of this Statement for their Transactions.\ + * Inverse of `financedBy`. + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: WalletStatement::class, inversedBy: 'financedBy')] + private Collection $financesTo; + + public function __construct() + { + $this->financedBy = new ArrayCollection(); + $this->financesTo = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTransaction(): ?AccountingTransaction + { + return $this->transaction; + } + + public function setTransaction(AccountingTransaction $transaction): static + { + $this->transaction = $transaction; + + return $this; + } + + public function getDirection(): ?WalletStatementDirection + { + return $this->direction; + } + + public function setDirection(WalletStatementDirection $direction): static + { + $this->direction = $direction; + + return $this; + } + + /** + * @return Collection + */ + public function getFinancedBy(): Collection + { + return $this->financedBy; + } + + public function addFinancedBy(WalletStatement $financedBy): static + { + if (!$this->financedBy->contains($financedBy)) { + $this->financedBy->add($financedBy); + $financedBy->addFinanceTo($this); + } + + return $this; + } + + public function removeFinancedBy(WalletStatement $financedBy): static + { + if ($this->financedBy->removeElement($financedBy)) { + $financedBy->removeFinanceTo($this); + } + + return $this; + } + + /** + * @return Collection + */ + public function getFinancesTo(): Collection + { + return $this->financesTo; + } + + public function addFinanceTo(WalletStatement $finance): static + { + if (!$this->financesTo->contains($finance)) { + $this->financesTo->add($finance); + } + + return $this; + } + + public function removeFinanceTo(WalletStatement $finance): static + { + $this->financesTo->removeElement($finance); + + return $this; + } +} diff --git a/src/Entity/WalletStatementDirection.php b/src/Entity/WalletStatementDirection.php new file mode 100644 index 0000000..f9dd281 --- /dev/null +++ b/src/Entity/WalletStatementDirection.php @@ -0,0 +1,16 @@ + + */ +class WalletStatementRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, WalletStatement::class); + } + + /** + * @return WalletStatement[] + */ + public function findByOrigin(Accounting $accounting): array + { + return $this->createQueryBuilder('w') + ->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction') + ->andWhere('wt.origin = :val') + ->setParameter('val', $accounting->getId()) + ->orderBy('w.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return WalletStatement[] + */ + public function findByTarget(Accounting $accounting): array + { + return $this->createQueryBuilder('w') + ->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction') + ->andWhere('wt.target = :val') + ->setParameter('val', $accounting->getId()) + ->orderBy('w.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + // /** + // * @return WalletStatement[] Returns an array of WalletStatement objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('w') + // ->andWhere('w.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('w.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?WalletStatement + // { + // return $this->createQueryBuilder('w') + // ->andWhere('w.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} From 9018652ba9c06c14e88e75436475db1a0b337427 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 14 Oct 2024 15:18:09 +0200 Subject: [PATCH 13/86] Create wallet statements for user-received transactions --- .../WalletTransactionsListener.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/EventListener/WalletTransactionsListener.php diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php new file mode 100644 index 0000000..dfb2a2a --- /dev/null +++ b/src/EventListener/WalletTransactionsListener.php @@ -0,0 +1,40 @@ +getTarget(); + if ($target->getOwnerClass() !== User::class) { + return; + } + + $statement = new WalletStatement(); + $statement->setTransaction($transaction); + $statement->setDirection(WalletStatementDirection::Incoming); + + $this->entityManager->persist($statement); + $this->entityManager->flush(); + } +} From 1f4f755f5f354ce20b4d03d517fb0bcba8e1d827 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Oct 2024 13:46:52 +0200 Subject: [PATCH 14/86] Make benzina pump command skip-pumped option negatable --- src/Command/BenzinaPumpCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 4b6ac86..ad717a3 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -50,7 +50,7 @@ class BenzinaPumpCommand extends Command $this->addOption( 'skip-pumped', null, - InputOption::VALUE_OPTIONAL, + InputOption::VALUE_NEGATABLE, 'Skips feeding already pumped records in a batch', false ); From 371d587dcdb95fee5d6a954623ab5e1c738f8986 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Oct 2024 13:48:59 +0200 Subject: [PATCH 15/86] Clearer acc <-> trx relationship --- src/Entity/Accounting.php | 54 ++++++++++--------- src/Entity/AccountingTransaction.php | 8 +-- .../AccountingTransactionsListener.php | 4 +- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index 4517e11..deb6508 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -32,18 +32,20 @@ class Accounting private ?string $currency = null; /** + * Transactions which originated from this Accounting. + * * @var Collection */ - #[API\ApiProperty(writable: false)] #[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)] - private Collection $transactionsIssued; + private Collection $transactionsOutgoing; /** + * Transactions which targeted this Accounting. + * * @var Collection */ - #[API\ApiProperty(writable: false)] #[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)] - private Collection $transactionsReceived; + private Collection $transactionsIncoming; #[API\ApiProperty(writable: false, readable: false)] #[ORM\Column(length: 255)] @@ -68,8 +70,8 @@ class Accounting * ideally a configuration that can be updated via a frontend, not env var only */ $this->currency = 'EUR'; - $this->transactionsIssued = new ArrayCollection(); - $this->transactionsReceived = new ArrayCollection(); + $this->transactionsOutgoing = new ArrayCollection(); + $this->transactionsIncoming = new ArrayCollection(); } public function getId(): ?int @@ -92,27 +94,27 @@ class Accounting /** * @return Collection */ - public function getTransactionsIssued(): Collection + public function getTransactionsOutgoing(): Collection { - return $this->transactionsIssued; + return $this->transactionsOutgoing; } - public function addTransactionsIssued(AccountingTransaction $transactionsIssued): static + public function addTransactionsOutgoing(AccountingTransaction $transactionsOutgoing): static { - if (!$this->transactionsIssued->contains($transactionsIssued)) { - $this->transactionsIssued->add($transactionsIssued); - $transactionsIssued->setOrigin($this); + if (!$this->transactionsOutgoing->contains($transactionsOutgoing)) { + $this->transactionsOutgoing->add($transactionsOutgoing); + $transactionsOutgoing->setOrigin($this); } return $this; } - public function removeTransactionsIssued(AccountingTransaction $transactionsIssued): static + public function removeTransactionsOutgoing(AccountingTransaction $transactionsOutgoing): static { - if ($this->transactionsIssued->removeElement($transactionsIssued)) { + if ($this->transactionsOutgoing->removeElement($transactionsOutgoing)) { // set the owning side to null (unless already changed) - if ($transactionsIssued->getOrigin() === $this) { - $transactionsIssued->setOrigin(null); + if ($transactionsOutgoing->getOrigin() === $this) { + $transactionsOutgoing->setOrigin(null); } } @@ -122,27 +124,27 @@ class Accounting /** * @return Collection */ - public function getTransactionsReceived(): Collection + public function getTransactionsIncoming(): Collection { - return $this->transactionsReceived; + return $this->transactionsIncoming; } - public function addTransactionsReceived(AccountingTransaction $transactionsReceived): static + public function addTransactionsIncoming(AccountingTransaction $transactionsIncoming): static { - if (!$this->transactionsReceived->contains($transactionsReceived)) { - $this->transactionsReceived->add($transactionsReceived); - $transactionsReceived->setTarget($this); + if (!$this->transactionsIncoming->contains($transactionsIncoming)) { + $this->transactionsIncoming->add($transactionsIncoming); + $transactionsIncoming->setTarget($this); } return $this; } - public function removeTransactionsReceived(AccountingTransaction $transactionsReceived): static + public function removeTransactionsIncoming(AccountingTransaction $transactionsIncoming): static { - if ($this->transactionsReceived->removeElement($transactionsReceived)) { + if ($this->transactionsIncoming->removeElement($transactionsIncoming)) { // set the owning side to null (unless already changed) - if ($transactionsReceived->getTarget() === $this) { - $transactionsReceived->setTarget(null); + if ($transactionsIncoming->getTarget() === $this) { + $transactionsIncoming->setTarget(null); } } diff --git a/src/Entity/AccountingTransaction.php b/src/Entity/AccountingTransaction.php index 6cc43a6..a4fd2ab 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/AccountingTransaction.php @@ -34,16 +34,16 @@ class AccountingTransaction private ?Money $money = null; /** - * The Accounting issuing the money of this Transaction. + * The Accounting from which the Transaction comes from. */ - #[ORM\ManyToOne(inversedBy: 'transactionsIssued', cascade: ['persist'])] + #[ORM\ManyToOne(inversedBy: 'transactionsOutgoing')] #[ORM\JoinColumn(nullable: false)] private ?Accounting $origin = null; /** - * The Accounting receiving the money of this Transaction. + * The Accounting where the Transaction goes to. */ - #[ORM\ManyToOne(inversedBy: 'transactionsReceived', cascade: ['persist'])] + #[ORM\ManyToOne(inversedBy: 'transactionsIncoming')] #[ORM\JoinColumn(nullable: false)] private ?Accounting $target = null; diff --git a/src/EventListener/AccountingTransactionsListener.php b/src/EventListener/AccountingTransactionsListener.php index 423a275..dc0d17a 100644 --- a/src/EventListener/AccountingTransactionsListener.php +++ b/src/EventListener/AccountingTransactionsListener.php @@ -20,12 +20,12 @@ final class AccountingTransactionsListener PostPersistEventArgs $event, ) { $origin = $transaction->getOrigin(); - $origin->addTransactionsIssued($transaction); + $origin->addTransactionsOutgoing($transaction); $this->entityManager->persist($origin); $target = $transaction->getTarget(); - $target->addTransactionsReceived($transaction); + $target->addTransactionsIncoming($transaction); $this->entityManager->persist($target); From 7c7310cf884e49e836efa395673afdb82520fc20 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 15 Oct 2024 13:49:34 +0200 Subject: [PATCH 16/86] Prove accountings dont cascade persist from transactions but do get updated --- tests/Entity/AccountingTest.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php index 29fde8c..3dd73a6 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/AccountingTest.php @@ -70,12 +70,17 @@ class AccountingTest extends KernelTestCase $accountingA = new Accounting(); $accountingA->setTipjar($tipjarA); + $this->entityManager->persist($accountingA); + $tipjarB = new Tipjar(); $tipjarB->setName('TEST_TIPJAR_B'); $accountingB = new Accounting(); $accountingB->setTipjar($tipjarB); + $this->entityManager->persist($accountingB); + $this->entityManager->flush(); + $transaction = new AccountingTransaction(); $transaction->setMoney(new Money(120, 'EUR')); $transaction->setOrigin($accountingA); @@ -84,26 +89,28 @@ class AccountingTest extends KernelTestCase $this->entityManager->persist($transaction); $this->entityManager->flush(); + /** @var Accounting */ $accountingA = $this->entityManager ->getRepository(Tipjar::class) ->findOneBy(['name' => 'TEST_TIPJAR_A']) ->getAccounting(); + /** @var Accounting */ $accountingB = $this->entityManager ->getRepository(Tipjar::class) ->findOneBy(['name' => 'TEST_TIPJAR_B']) ->getAccounting(); - $this->assertCount(1, $accountingA->getTransactionsIssued()); - $this->assertCount(0, $accountingA->getTransactionsReceived()); + $this->assertCount(1, $accountingA->getTransactionsOutgoing()); + $this->assertCount(0, $accountingA->getTransactionsIncoming()); - $this->assertCount(0, $accountingB->getTransactionsIssued()); - $this->assertCount(1, $accountingB->getTransactionsReceived()); + $this->assertCount(0, $accountingB->getTransactionsOutgoing()); + $this->assertCount(1, $accountingB->getTransactionsIncoming()); - $this->assertEquals(120, $accountingA->getTransactionsIssued()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accountingA->getTransactionsIssued()[0]->getMoney()->currency); + $this->assertEquals(120, $accountingA->getTransactionsOutgoing()[0]->getMoney()->amount); + $this->assertEquals('EUR', $accountingA->getTransactionsOutgoing()[0]->getMoney()->currency); - $this->assertEquals(120, $accountingB->getTransactionsReceived()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accountingB->getTransactionsReceived()[0]->getMoney()->currency); + $this->assertEquals(120, $accountingB->getTransactionsIncoming()[0]->getMoney()->amount); + $this->assertEquals('EUR', $accountingB->getTransactionsIncoming()[0]->getMoney()->currency); } } From 2771bc44c97baaf4a8be47247646eb8d886a0dae Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 21 Oct 2024 09:31:40 +0200 Subject: [PATCH 17/86] Update GatewayCharge properties --- src/Entity/GatewayCharge.php | 70 ++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index c78735f..0263bd1 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -4,6 +4,7 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Repository\GatewayChargeRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -14,27 +15,30 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: GatewayChargeRepository::class)] class GatewayCharge { - // TO-DO: This message should be translatable to the User's language - public const MESSAGE_STATEMENT = 'PAGO EN GOTEO.ORG'; - #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; /** - * The type of a GatewayCharge represents the kind of payment. + * The type represents the kind of payment for the charged money. */ #[Assert\NotBlank()] #[ORM\Column()] private ?GatewayChargeType $type = null; /** - * The charged monetary sum. + * A short, descriptive text for this charge operation. */ #[Assert\NotBlank()] - #[ORM\Embedded(Money::class)] - private ?Money $money = null; + #[ORM\Column(length: 255)] + private ?string $title = null; + + /** + * Detailed message about this charge operation. + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $description = null; /** * The Accounting receiving the consequent Transaction for this GatewayCharge. @@ -45,11 +49,11 @@ class GatewayCharge private ?Accounting $target = null; /** - * The AccountingTransaction generated for this charge after the checkout with the Gateway. + * The charged monetary sum. */ - #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(cascade: ['persist', 'remove'])] - private ?AccountingTransaction $transaction = null; + #[Assert\NotBlank()] + #[ORM\Embedded(Money::class)] + private ?Money $money = null; public function getId(): ?int { @@ -68,18 +72,6 @@ class GatewayCharge return $this; } - public function getMoney(): ?Money - { - return $this->money; - } - - public function setMoney(Money $money): static - { - $this->money = $money; - - return $this; - } - public function getTarget(): ?Accounting { return $this->target; @@ -92,14 +84,38 @@ class GatewayCharge return $this; } - public function getTransaction(): ?AccountingTransaction + public function getMoney(): ?Money { - return $this->transaction; + return $this->money; } - public function setTransaction(?AccountingTransaction $transaction): static + public function setMoney(Money $money): static { - $this->transaction = $transaction; + $this->money = $money; + + 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; } From 9681818ec0e4d7cfbbc49fe87bc8699e3e0fff4a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 22 Oct 2024 13:16:59 +0200 Subject: [PATCH 18/86] Update GatewayCharge to have title, description and hold related transactions --- src/Entity/GatewayCharge.php | 55 ++++++++++++++++++- src/Entity/GatewayCheckout.php | 52 ++++++++++-------- src/Library/Benzina/Pump/CheckoutsPump.php | 7 +++ src/Library/Economy/Payment/PaypalGateway.php | 4 +- src/Library/Economy/Payment/StripeGateway.php | 4 +- src/Service/GatewayCheckoutService.php | 2 +- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index 0263bd1..b4a3322 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -4,6 +4,8 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Repository\GatewayChargeRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -11,7 +13,6 @@ use Symfony\Component\Validator\Constraints as Assert; /** * A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway. */ -#[API\ApiResource()] #[ORM\Entity(repositoryClass: GatewayChargeRepository::class)] class GatewayCharge { @@ -20,6 +21,10 @@ class GatewayCharge #[ORM\Column] private ?int $id = null; + #[API\ApiProperty(readable: false, writable: false)] + #[ORM\ManyToOne(inversedBy: 'charges')] + private ?GatewayCheckout $checkout = null; + /** * The type represents the kind of payment for the charged money. */ @@ -55,11 +60,35 @@ class GatewayCharge #[ORM\Embedded(Money::class)] private ?Money $money = null; + /** + * @var Collection + */ + #[API\ApiProperty(readableLink: false, writable: false)] + #[ORM\ManyToMany(targetEntity: AccountingTransaction::class, cascade: ['persist'])] + private Collection $transactions; + + public function __construct() + { + $this->transactions = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; } + public function getCheckout(): ?GatewayCheckout + { + return $this->checkout; + } + + public function setCheckout(?GatewayCheckout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } + public function getType(): ?GatewayChargeType { return $this->type; @@ -119,4 +148,28 @@ class GatewayCharge return $this; } + + /** + * @return Collection + */ + public function getTransactions(): Collection + { + return $this->transactions; + } + + public function addTransaction(AccountingTransaction $transaction): static + { + if (!$this->transactions->contains($transaction)) { + $this->transactions->add($transaction); + } + + return $this; + } + + public function removeTransaction(AccountingTransaction $transaction): static + { + $this->transactions->removeElement($transaction); + + return $this; + } } diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index d483ce7..09ff5b7 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -46,14 +46,6 @@ class GatewayCheckout #[ORM\JoinColumn(nullable: false)] private ?Accounting $origin = null; - /** - * The GatewayCharges to be charged at checkout with the Gateway. - */ - #[Assert\NotBlank()] - #[Assert\Count(min: 1)] - #[ORM\ManyToMany(targetEntity: GatewayCharge::class, cascade: ['persist'])] - private Collection $charges; - /** * The status of the checkout with the Gateway. */ @@ -63,6 +55,16 @@ class GatewayCheckout #[ORM\Column()] private ?GatewayCheckoutStatus $status = null; + /** + * The GatewayCharges to be charged at checkout with the gateway. + * + * @var Collection + */ + #[Assert\NotBlank()] + #[Assert\Count(min: 1)] + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCharge::class, cascade: ['persist'])] + private Collection $charges; + /** * The name of the Gateway implementation to checkout with. */ @@ -114,8 +116,8 @@ class GatewayCheckout public function __construct() { - $this->charges = new ArrayCollection(); $this->status = GatewayCheckoutStatus::Pending; + $this->charges = new ArrayCollection(); } public function getId(): ?int @@ -135,6 +137,18 @@ class GatewayCheckout return $this; } + public function getStatus(): ?GatewayCheckoutStatus + { + return $this->status; + } + + public function setStatus(GatewayCheckoutStatus $status): static + { + $this->status = $status; + + return $this; + } + /** * @return Collection */ @@ -147,6 +161,7 @@ class GatewayCheckout { if (!$this->charges->contains($charge)) { $this->charges->add($charge); + $charge->setCheckout($this); } return $this; @@ -154,19 +169,12 @@ class GatewayCheckout public function removeCharge(GatewayCharge $charge): static { - $this->charges->removeElement($charge); - - return $this; - } - - public function getStatus(): ?GatewayCheckoutStatus - { - return $this->status; - } - - public function setStatus(GatewayCheckoutStatus $status): static - { - $this->status = $status; + if ($this->charges->removeElement($charge)) { + // set the owning side to null (unless already changed) + if ($charge->getCheckout() === $this) { + $charge->setCheckout(null); + } + } return $this; } diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index b1fa028..8d412b4 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -34,6 +34,10 @@ class CheckoutsPump extends AbstractPump implements PumpInterface public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction'; public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval'; + public const CHARGE_TITLE_PROJECT = 'Pago en Goteo v3 - Donación a proyecto'; + 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_TIPJAR_NAME = 'platform'; private const MAX_INT = 2147483647; @@ -131,12 +135,14 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); if (empty($record['project'])) { + $charge->setTitle(self::CHARGE_TITLE_POOL); $charge->setTarget($user->getAccounting()); } if (!empty($record['project'])) { $project = $projects[$record['project']]; + $charge->setTitle(self::CHARGE_TITLE_PROJECT); $charge->setTarget($project->getAccounting()); } @@ -145,6 +151,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface if ($record['donate_amount'] > 0) { $tip = new GatewayCharge(); $tip->setType(GatewayChargeType::Single); + $tip->setTitle(self::CHARGE_TITLE_TIP); $tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency'])); $tip->setTarget($tipjar->getAccounting()); diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index d574fc8..1f9f542 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -214,8 +214,8 @@ class PaypalGateway implements GatewayInterface 'custom_id' => $reference, 'items' => [ [ - 'name' => $charge::MESSAGE_STATEMENT, - 'description' => $charge::MESSAGE_STATEMENT, + 'name' => $charge->getTitle(), + 'description' => $charge->getDescription(), 'quantity' => '1', 'unit_amount' => [ ...$money, diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 48a4e74..5060537 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -152,8 +152,8 @@ class StripeGateway implements GatewayInterface 'currency' => $charge->getMoney()->currency, 'unit_amount' => $charge->getMoney()->amount, 'product_data' => [ - 'name' => $charge::MESSAGE_STATEMENT, - 'statement_descriptor' => $charge::MESSAGE_STATEMENT, + 'name' => $charge->getTitle(), + 'statement_descriptor' => $charge->getDescription(), ], ]; diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 7939bb7..0d5efaa 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -99,7 +99,7 @@ class GatewayCheckoutService $transaction->setOrigin($checkout->getOrigin()); $transaction->setTarget($charge->getTarget()); - $charge->setTransaction($transaction); + $charge->addTransaction($transaction); } return $checkout; From 59061bf235c67c916e6113fe95974b3a108bd73a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 22 Oct 2024 14:27:03 +0200 Subject: [PATCH 19/86] Fix whitespace --- src/Entity/GatewayCheckout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 09ff5b7..84dce4d 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -57,7 +57,7 @@ class GatewayCheckout /** * The GatewayCharges to be charged at checkout with the gateway. - * + * * @var Collection */ #[Assert\NotBlank()] From ffa74ca0f17142aa1e7fcdc72a79a195ba9a6bb6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 12:43:28 +0200 Subject: [PATCH 20/86] Bump minor versions of outdated deps --- composer.lock | 152 ++++++++++++++++++++++++++------------------------ 1 file changed, 79 insertions(+), 73 deletions(-) diff --git a/composer.lock b/composer.lock index 54545f8..7d3f381 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "20e503cd4b7c41eca5ee6fcf83fa4ae8", + "content-hash": "0d0e2940f9aaecab4ba3c50bc5822de6", "packages": [ { "name": "api-platform/core", - "version": "v3.4.1", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "05f7d795252d32424b49336e5184557569809796" + "reference": "77d3ff3e8ddf88546eaf288a399d11f130553606" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/05f7d795252d32424b49336e5184557569809796", - "reference": "05f7d795252d32424b49336e5184557569809796", + "url": "https://api.github.com/repos/api-platform/core/zipball/77d3ff3e8ddf88546eaf288a399d11f130553606", + "reference": "77d3ff3e8ddf88546eaf288a399d11f130553606", "shasum": "" }, "require": { @@ -223,9 +223,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.4.1" + "source": "https://github.com/api-platform/core/tree/v3.4.4" }, - "time": "2024-09-23T07:27:56+00:00" + "time": "2024-10-19T10:05:07+00:00" }, { "name": "beberlei/doctrineextensions", @@ -708,16 +708,16 @@ }, { "name": "doctrine/common", - "version": "3.4.4", + "version": "3.4.5", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "url": "https://api.github.com/repos/doctrine/common/zipball/6c8fef961f67b8bc802ce3e32e3ebd1022907286", + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286", "shasum": "" }, "require": { @@ -779,7 +779,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.4" + "source": "https://github.com/doctrine/common/tree/3.4.5" }, "funding": [ { @@ -795,20 +795,20 @@ "type": "tidelift" } ], - "time": "2024-04-16T13:35:33+00:00" + "time": "2024-10-08T15:53:43+00:00" }, { "name": "doctrine/dbal", - "version": "3.9.1", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -824,7 +824,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -892,7 +892,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.1" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -908,7 +908,7 @@ "type": "tidelift" } ], - "time": "2024-09-01T13:49:23+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -1500,16 +1500,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.1", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877" + "reference": "5007eb1168691225ac305fe16856755c20860842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/7760fbd0b7cb58bfb50415505a7bab821adf0877", - "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", + "reference": "5007eb1168691225ac305fe16856755c20860842", "shasum": "" }, "require": { @@ -1583,7 +1583,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.1" + "source": "https://github.com/doctrine/migrations/tree/3.8.2" }, "funding": [ { @@ -1599,20 +1599,20 @@ "type": "tidelift" } ], - "time": "2024-08-28T13:17:28+00:00" + "time": "2024-10-10T21:35:27+00:00" }, { "name": "doctrine/orm", - "version": "2.19.7", + "version": "2.20.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "168ac31084226f94d42e7461a40ff5607a56bd35" + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/168ac31084226f94d42e7461a40ff5607a56bd35", - "reference": "168ac31084226f94d42e7461a40ff5607a56bd35", + "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c", + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c", "shasum": "" }, "require": { @@ -1641,7 +1641,9 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.11.1", + "phpstan/extension-installer": "~1.1.0 || ^1.4", + "phpstan/phpstan": "~1.4.10 || 1.12.6", + "phpstan/phpstan-deprecation-rules": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", @@ -1698,9 +1700,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.7" + "source": "https://github.com/doctrine/orm/tree/2.20.0" }, - "time": "2024-08-23T06:54:57+00:00" + "time": "2024-10-11T11:47:24+00:00" }, { "name": "doctrine/persistence", @@ -1801,16 +1803,16 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.4.1", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "7f83911cc5eba870de7ebb11283972483f7e2891" + "reference": "b784cbde727cf806721451dde40eff4fec3bbe86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/7f83911cc5eba870de7ebb11283972483f7e2891", - "reference": "7f83911cc5eba870de7ebb11283972483f7e2891", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b784cbde727cf806721451dde40eff4fec3bbe86", + "reference": "b784cbde727cf806721451dde40eff4fec3bbe86", "shasum": "" }, "require": { @@ -1818,6 +1820,7 @@ }, "require-dev": { "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.5", "vimeo/psalm": "^5.24" @@ -1850,22 +1853,22 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.4.1" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.1" }, - "time": "2024-08-05T20:32:22+00:00" + "time": "2024-10-21T18:21:57+00:00" }, { "name": "doctrineencryptbundle/doctrine-encrypt-bundle", - "version": "5.4.0", + "version": "5.4.1", "source": { "type": "git", "url": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle.git", - "reference": "aa60745e1548213f11467a1d41308d6c03f23faf" + "reference": "556c2ec0156d519378e6d62a61c2e924e4583e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/aa60745e1548213f11467a1d41308d6c03f23faf", - "reference": "aa60745e1548213f11467a1d41308d6c03f23faf", + "url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/556c2ec0156d519378e6d62a61c2e924e4583e13", + "reference": "556c2ec0156d519378e6d62a61c2e924e4583e13", "shasum": "" }, "require": { @@ -1915,22 +1918,22 @@ ], "support": { "issues": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/issues", - "source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.0" + "source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.1" }, - "time": "2024-03-02T18:14:49+00:00" + "time": "2024-10-13T10:08:12+00:00" }, { "name": "gedmo/doctrine-extensions", - "version": "v3.16.1", + "version": "v3.17.1", "source": { "type": "git", "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d" + "reference": "eabb45018c5a4362b46c5beae3881261da89f900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/e85560ed96f977b8c29428a99222cb2ef2f0e80d", - "reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900", + "reference": "eabb45018c5a4362b46c5beae3881261da89f900", "shasum": "" }, "require": { @@ -1947,14 +1950,14 @@ }, "conflict": { "doctrine/annotations": "<1.13 || >=3.0", - "doctrine/dbal": "<3.2 || >=4.0", + "doctrine/dbal": "<3.7 || >=5.0", "doctrine/mongodb-odm": "<2.3 || >=3.0", "doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=4.0" }, "require-dev": { "doctrine/annotations": "^1.13 || ^2.0", "doctrine/cache": "^1.11 || ^2.0", - "doctrine/dbal": "^3.2", + "doctrine/dbal": "^3.7 || ^4.0", "doctrine/doctrine-bundle": "^2.3", "doctrine/mongodb-odm": "^2.3", "doctrine/orm": "^2.14.0 || ^3.0", @@ -2026,7 +2029,7 @@ "support": { "email": "gediminas.morkevicius@gmail.com", "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", - "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.16.1", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1", "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" }, "funding": [ @@ -2047,7 +2050,7 @@ "type": "github" } ], - "time": "2024-06-25T16:22:14+00:00" + "time": "2024-10-07T22:30:27+00:00" }, { "name": "godruoyi/php-snowflake", @@ -2737,16 +2740,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.31.0", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", - "reference": "249f15fb843bf240cf058372dad29e100cee6c17", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", "shasum": "" }, "require": { @@ -2778,9 +2781,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2024-09-22T11:32:18+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "psr/cache", @@ -4512,22 +4515,25 @@ }, { "name": "symfony/flex", - "version": "v2.4.6", + "version": "v2.4.7", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b" + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/4dc11919791f81d087a12db2ab4c7e044431ef6b", - "reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b", + "url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402", + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402", "shasum": "" }, "require": { "composer-plugin-api": "^2.1", "php": ">=8.0" }, + "conflict": { + "composer/semver": "<1.7.2" + }, "require-dev": { "composer/composer": "^2.1", "symfony/dotenv": "^5.4|^6.0", @@ -4557,7 +4563,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.6" + "source": "https://github.com/symfony/flex/tree/v2.4.7" }, "funding": [ { @@ -4573,7 +4579,7 @@ "type": "tidelift" } ], - "time": "2024-04-27T10:22:22+00:00" + "time": "2024-10-07T08:51:54+00:00" }, { "name": "symfony/framework-bundle", @@ -8293,16 +8299,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -8345,9 +8351,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phar-io/manifest", @@ -11146,7 +11152,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -11154,6 +11160,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } From fee7bb852e5188be3f0813083ffc484c3057e702 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 12:51:21 +0200 Subject: [PATCH 21/86] Make GatewayCharge output as IRI strings on GatewayCheckout --- src/Entity/GatewayCharge.php | 1 + src/Entity/GatewayCheckout.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index b4a3322..49b6ad5 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway. */ +#[API\Get()] #[ORM\Entity(repositoryClass: GatewayChargeRepository::class)] class GatewayCharge { diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 84dce4d..40ca6cc 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -60,6 +60,7 @@ class GatewayCheckout * * @var Collection */ + #[API\ApiProperty(readableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCharge::class, cascade: ['persist'])] From 98dc37baf92e1e43dd96983d8cf631f98f1e3b84 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 13:39:08 +0200 Subject: [PATCH 22/86] Remove inverse side of acc <-> trxs relationship --- src/Entity/Accounting.php | 80 ---------------------------- src/Entity/AccountingTransaction.php | 4 +- tests/Entity/AccountingTest.php | 54 ------------------- 3 files changed, 2 insertions(+), 136 deletions(-) diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index deb6508..691d37a 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -5,8 +5,6 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Repository\AccountingRepository; use App\Service\ApiResourceNormalizer; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -31,22 +29,6 @@ class Accounting #[ORM\Column(length: 3)] private ?string $currency = null; - /** - * Transactions which originated from this Accounting. - * - * @var Collection - */ - #[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)] - private Collection $transactionsOutgoing; - - /** - * Transactions which targeted this Accounting. - * - * @var Collection - */ - #[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)] - private Collection $transactionsIncoming; - #[API\ApiProperty(writable: false, readable: false)] #[ORM\Column(length: 255)] private ?string $ownerClass = null; @@ -70,8 +52,6 @@ class Accounting * ideally a configuration that can be updated via a frontend, not env var only */ $this->currency = 'EUR'; - $this->transactionsOutgoing = new ArrayCollection(); - $this->transactionsIncoming = new ArrayCollection(); } public function getId(): ?int @@ -91,66 +71,6 @@ class Accounting return $this; } - /** - * @return Collection - */ - public function getTransactionsOutgoing(): Collection - { - return $this->transactionsOutgoing; - } - - public function addTransactionsOutgoing(AccountingTransaction $transactionsOutgoing): static - { - if (!$this->transactionsOutgoing->contains($transactionsOutgoing)) { - $this->transactionsOutgoing->add($transactionsOutgoing); - $transactionsOutgoing->setOrigin($this); - } - - return $this; - } - - public function removeTransactionsOutgoing(AccountingTransaction $transactionsOutgoing): static - { - if ($this->transactionsOutgoing->removeElement($transactionsOutgoing)) { - // set the owning side to null (unless already changed) - if ($transactionsOutgoing->getOrigin() === $this) { - $transactionsOutgoing->setOrigin(null); - } - } - - return $this; - } - - /** - * @return Collection - */ - public function getTransactionsIncoming(): Collection - { - return $this->transactionsIncoming; - } - - public function addTransactionsIncoming(AccountingTransaction $transactionsIncoming): static - { - if (!$this->transactionsIncoming->contains($transactionsIncoming)) { - $this->transactionsIncoming->add($transactionsIncoming); - $transactionsIncoming->setTarget($this); - } - - return $this; - } - - public function removeTransactionsIncoming(AccountingTransaction $transactionsIncoming): static - { - if ($this->transactionsIncoming->removeElement($transactionsIncoming)) { - // set the owning side to null (unless already changed) - if ($transactionsIncoming->getTarget() === $this) { - $transactionsIncoming->setTarget(null); - } - } - - return $this; - } - public function getOwnerClass(): ?string { return $this->ownerClass; diff --git a/src/Entity/AccountingTransaction.php b/src/Entity/AccountingTransaction.php index a4fd2ab..8823a62 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/AccountingTransaction.php @@ -36,14 +36,14 @@ class AccountingTransaction /** * The Accounting from which the Transaction comes from. */ - #[ORM\ManyToOne(inversedBy: 'transactionsOutgoing')] + #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] private ?Accounting $origin = null; /** * The Accounting where the Transaction goes to. */ - #[ORM\ManyToOne(inversedBy: 'transactionsIncoming')] + #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] private ?Accounting $target = null; diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php index 3dd73a6..df69e3b 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/AccountingTest.php @@ -3,8 +3,6 @@ namespace App\Tests\Entity; use App\Entity\Accounting; -use App\Entity\AccountingTransaction; -use App\Entity\Money; use App\Entity\Tipjar; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -61,56 +59,4 @@ class AccountingTest extends KernelTestCase $this->assertEquals(Tipjar::class, $accountingB->getOwnerClass()); $this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName()); } - - public function testAccountingGetsUpdatedByTransactions() - { - $tipjarA = new Tipjar(); - $tipjarA->setName('TEST_TIPJAR_A'); - - $accountingA = new Accounting(); - $accountingA->setTipjar($tipjarA); - - $this->entityManager->persist($accountingA); - - $tipjarB = new Tipjar(); - $tipjarB->setName('TEST_TIPJAR_B'); - - $accountingB = new Accounting(); - $accountingB->setTipjar($tipjarB); - - $this->entityManager->persist($accountingB); - $this->entityManager->flush(); - - $transaction = new AccountingTransaction(); - $transaction->setMoney(new Money(120, 'EUR')); - $transaction->setOrigin($accountingA); - $transaction->setTarget($accountingB); - - $this->entityManager->persist($transaction); - $this->entityManager->flush(); - - /** @var Accounting */ - $accountingA = $this->entityManager - ->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_A']) - ->getAccounting(); - - /** @var Accounting */ - $accountingB = $this->entityManager - ->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_B']) - ->getAccounting(); - - $this->assertCount(1, $accountingA->getTransactionsOutgoing()); - $this->assertCount(0, $accountingA->getTransactionsIncoming()); - - $this->assertCount(0, $accountingB->getTransactionsOutgoing()); - $this->assertCount(1, $accountingB->getTransactionsIncoming()); - - $this->assertEquals(120, $accountingA->getTransactionsOutgoing()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accountingA->getTransactionsOutgoing()[0]->getMoney()->currency); - - $this->assertEquals(120, $accountingB->getTransactionsIncoming()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accountingB->getTransactionsIncoming()[0]->getMoney()->currency); - } } From 99e9ae28e7480b98ff4f37febcee0fed9add84a5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 14:06:19 +0200 Subject: [PATCH 23/86] Allow to send charges data in checkout --- src/Entity/GatewayCheckout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 40ca6cc..8ca05d4 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -60,7 +60,7 @@ class GatewayCheckout * * @var Collection */ - #[API\ApiProperty(readableLink: true)] + #[API\ApiProperty(readableLink: true, writableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCharge::class, cascade: ['persist'])] From f512754016b4d3a519d2881b1a9f4a0b28f6c1e8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 14:06:38 +0200 Subject: [PATCH 24/86] Remove accounting transactions listener --- .../AccountingTransactionsListener.php | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/EventListener/AccountingTransactionsListener.php diff --git a/src/EventListener/AccountingTransactionsListener.php b/src/EventListener/AccountingTransactionsListener.php deleted file mode 100644 index dc0d17a..0000000 --- a/src/EventListener/AccountingTransactionsListener.php +++ /dev/null @@ -1,34 +0,0 @@ -getOrigin(); - $origin->addTransactionsOutgoing($transaction); - - $this->entityManager->persist($origin); - - $target = $transaction->getTarget(); - $target->addTransactionsIncoming($transaction); - - $this->entityManager->persist($target); - - $this->entityManager->flush(); - } -} From c7b4ab0feebeb860de86ec4b514fa5fc8c9bb4ef Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 14:31:50 +0200 Subject: [PATCH 25/86] Validate the charge types support in a checkout with any given gateway --- src/Entity/GatewayCheckout.php | 2 + src/Validator/SupportedChargeTypes.php | 14 ++++++ .../SupportedChargeTypesValidator.php | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/Validator/SupportedChargeTypes.php create mode 100644 src/Validator/SupportedChargeTypesValidator.php diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 8ca05d4..b861cd0 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -9,6 +9,7 @@ use App\Entity\Trait\TimestampableUpdationEntity; use App\Repository\GatewayCheckoutRepository; use App\State\GatewayCheckoutProcessor; use App\Validator\GatewayName; +use App\Validator\SupportedChargeTypes; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -63,6 +64,7 @@ class GatewayCheckout #[API\ApiProperty(readableLink: true, writableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] + #[SupportedChargeTypes()] #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCharge::class, cascade: ['persist'])] private Collection $charges; diff --git a/src/Validator/SupportedChargeTypes.php b/src/Validator/SupportedChargeTypes.php new file mode 100644 index 0000000..b803659 --- /dev/null +++ b/src/Validator/SupportedChargeTypes.php @@ -0,0 +1,14 @@ + */ + $charges = $value; + + $checkout = $charges->toArray()[0]->getCheckout(); + $gateway = $this->gatewayLocator->getGatewayOf($checkout); + + foreach ($charges as $charge) { + if (!\in_array( + $charge->getType(), + $gateway->getSupportedChargeTypes() + )) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $charge->getType()->value) + ->setParameter('{{ gateway }}', $gateway->getName()) + ->addViolation(); + } + } + } +} From fb527db4d96d160d5d8f6d4d588a38fee39b6fac Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 15:05:02 +0200 Subject: [PATCH 26/86] Allow to retrieve wallet statements in either direction of an accounting --- src/Repository/WalletStatementRepository.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Repository/WalletStatementRepository.php b/src/Repository/WalletStatementRepository.php index 0e2c511..5277a5e 100644 --- a/src/Repository/WalletStatementRepository.php +++ b/src/Repository/WalletStatementRepository.php @@ -18,6 +18,22 @@ class WalletStatementRepository extends ServiceEntityRepository parent::__construct($registry, WalletStatement::class); } + /** + * @return WalletStatement[] + */ + public function findByAccounting(Accounting $accounting): array + { + return $this->createQueryBuilder('w') + ->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction') + ->where('wt.origin = :val') + ->orWhere('wt.target = :val') + ->setParameter('val', $accounting->getId()) + ->orderBy('w.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + /** * @return WalletStatement[] */ From dea00bf227eaa335400d4e403756f26126984ccf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 15:06:07 +0200 Subject: [PATCH 27/86] Add WalletGatewayService --- .../Economy/Payment/WalletGatewayService.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Library/Economy/Payment/WalletGatewayService.php diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Library/Economy/Payment/WalletGatewayService.php new file mode 100644 index 0000000..63fa5a8 --- /dev/null +++ b/src/Library/Economy/Payment/WalletGatewayService.php @@ -0,0 +1,44 @@ +getCurrency()); + $statements = $this->walletStatementRepository->findByAccounting($accounting); + + foreach ($statements as $statement) { + $transaction = $statement->getTransaction(); + $money = Brick\Money::ofMinor( + $transaction->getMoney()->amount, + $transaction->getMoney()->currency + ); + + switch ($statement->getDirection()) { + case WalletStatementDirection::Incoming: + $available = $available->plus($money); + break; + case WalletStatementDirection::Outgoing: + $available = $available->minus($money); + break; + } + } + + return new Money( + $available->getMinorAmount()->toInt(), + $available->getCurrency()->getCurrencyCode() + ); + } +} From 5fb64e1f2d22506a818fa3e05b5f09ee88300ba7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 23 Oct 2024 15:06:28 +0200 Subject: [PATCH 28/86] Code style improvements --- src/EventListener/WalletTransactionsListener.php | 6 +++++- src/Validator/SupportedChargeTypesValidator.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index dfb2a2a..c47f0e3 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -11,7 +11,11 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Events; -#[AsEntityListener(event: Events::postPersist, method: 'processTransaction', entity: AccountingTransaction::class)] +#[AsEntityListener( + event: Events::postPersist, + method: 'processTransaction', + entity: AccountingTransaction::class +)] final class WalletTransactionsListener { public function __construct( diff --git a/src/Validator/SupportedChargeTypesValidator.php b/src/Validator/SupportedChargeTypesValidator.php index 06c0f9a..c510f14 100644 --- a/src/Validator/SupportedChargeTypesValidator.php +++ b/src/Validator/SupportedChargeTypesValidator.php @@ -10,7 +10,7 @@ use Symfony\Component\Validator\ConstraintValidator; class SupportedChargeTypesValidator extends ConstraintValidator { public function __construct( - private GatewayLocator $gatewayLocator + private GatewayLocator $gatewayLocator, ) {} public function validate(mixed $value, Constraint $constraint): void From d242337b1147b3fdb1f991389cf6e3564821f1f6 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Oct 2024 14:48:35 +0200 Subject: [PATCH 29/86] Make exchange interface names be clearer --- .../Currency/EuropeanCentralBankExchange.php | 12 +++++----- .../Economy/Currency/ExchangeInterface.php | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php b/src/Library/Economy/Currency/EuropeanCentralBankExchange.php index 1fc9238..c1e260b 100644 --- a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php +++ b/src/Library/Economy/Currency/EuropeanCentralBankExchange.php @@ -125,11 +125,11 @@ class EuropeanCentralBankExchange implements ExchangeInterface return 100; } - public function getConversion(MoneyContainer $money, string $currency): EntityMoney + public function convert(MoneyContainer $money, string $toCurrency): EntityMoney { $converted = $this->converter->convert( $money, - $currency, + $toCurrency, null, RoundingMode::HALF_EVEN ); @@ -140,14 +140,14 @@ class EuropeanCentralBankExchange implements ExchangeInterface ); } - public function getConversionRate(string $source, string $target): float + public function getConversionRate(string $fromCurrency, string $toCurrency): float { - return $this->provider->getExchangeRate($source, $target)->toFloat(); + return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); } - public function getConversionDate(string $source, string $target): \DateTimeInterface + public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface { - $this->provider->getExchangeRate($source, $target); + $this->provider->getExchangeRate($fromCurrency, $toCurrency); return $this->date; } diff --git a/src/Library/Economy/Currency/ExchangeInterface.php b/src/Library/Economy/Currency/ExchangeInterface.php index 579bf03..d2e2f17 100644 --- a/src/Library/Economy/Currency/ExchangeInterface.php +++ b/src/Library/Economy/Currency/ExchangeInterface.php @@ -2,7 +2,7 @@ namespace App\Library\Economy\Currency; -use App\Entity\Money as EntityMoney; +use App\Entity\Money; use Brick\Money\Exception\CurrencyConversionException; use Brick\Money\MoneyContainer; @@ -19,32 +19,32 @@ interface ExchangeInterface public function getWeight(): int; /** - * @param MoneyContainer $money The money to be converted - * @param string $currency The currency to convert to + * @param MoneyContainer $money The money to be converted + * @param string $toCurrency The currency to convert to * - * @return EntityMoney The converted Money + * @return Money The converted Money * * @throws CurrencyConversionException If the exchange rate is not available */ - public function getConversion(MoneyContainer $money, string $currency): EntityMoney; + public function convert(MoneyContainer $money, string $toCurrency): Money; /** - * @param string $source The currency to convert from - * @param string $target The currency to convert to + * @param string $fromCurrency The currency to convert from + * @param string $toCurrency The currency to convert to * * @return float The rate of the conversion * * @throws CurrencyConversionException If the exchange rate is not available */ - public function getConversionRate(string $source, string $target): float; + public function getConversionRate(string $fromCurrency, string $toCurrency): float; /** - * @param string $source The currency to convert from - * @param string $target The currency to convert to + * @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 * * @throws CurrencyConversionException If the exchange rate is not available */ - public function getConversionDate(string $source, string $target): \DateTimeInterface; + public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface; } From 4843321f0e20766475de949bccc88faac13b313e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 24 Oct 2024 14:49:12 +0200 Subject: [PATCH 30/86] Add MoneyService for brick/money operations under the hood --- src/Library/Economy/MoneyService.php | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/Library/Economy/MoneyService.php diff --git a/src/Library/Economy/MoneyService.php b/src/Library/Economy/MoneyService.php new file mode 100644 index 0000000..b416bad --- /dev/null +++ b/src/Library/Economy/MoneyService.php @@ -0,0 +1,85 @@ +getMinorAmount()->toInt(), + $brick->getCurrency()->getCurrencyCode() + ); + } + + public static function toBrick(Money $money): Brick\Money + { + return Brick\Money::ofMinor($money->amount, $money->currency); + } + + /** + * Adds `a` to `b`. + */ + public function add(Money $a, Money $b): Money + { + $a = $this->convert($a, $b->currency); + $ab = self::toBrick($b)->plus($a); + + return self::toMoney($ab); + } + + /** + * Substracts `a` from `b`. + */ + public function substract(Money $a, Money $b): Money + { + $a = $this->convert($a, $b->currency); + $ab = self::toBrick($b)->minus($a); + + return self::toMoney($ab); + } + + /** + * Compares `a` to `b`. + * + * @return bool `true` if `a` is less than `b` + */ + public function isLessThan(Money $a, Money $b): bool + { + $a = $this->convert($a, $b->currency); + + return $a->isLessThan(self::toBrick($b)); + } + + /** + * Compaers `a` to `b`. + * + * @return bool `true` if `a` is more than `b` + */ + public function isGreaterThan(Money $a, Money $b): bool + { + $a = $this->convert($a, $b->currency); + + return $a->isGreaterThan(self::toBrick($b)); + } + + private function convert(Money $money, string $toCurrency): Brick\Money + { + $fromCurrency = $money->currency; + if ($fromCurrency === $toCurrency) { + return $money; + } + + $exchange = $this->exchangeLocator->getExchangeFor($fromCurrency, $toCurrency); + + return self::toBrick($exchange->convert(self::toBrick($money), $toCurrency)); + } +} From a76d59de74ea88a00bfc71d7ebe12cc86dcb3e6b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 10:57:28 +0200 Subject: [PATCH 31/86] Add testing for wallet gateway service --- .../Payment/WalletGatewayServiceTest.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/Library/Economy/Payment/WalletGatewayServiceTest.php diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php new file mode 100644 index 0000000..5f15a22 --- /dev/null +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -0,0 +1,84 @@ +entityManager = static::getContainer()->get(EntityManagerInterface::class); + $this->walletService = static::getContainer()->get(WalletGatewayService::class); + } + + private function getUserAccounting(): Accounting + { + $user = new User(); + $user->setUsername('wallettestuser'); + $user->setEmail('testuser@wallet.com'); + $user->setPassword('wallettestpassword'); + + $accounting = new Accounting(); + $accounting->setCurrency('EUR'); + $accounting->setUser($user); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $accounting; + } + + private function getTipjarAccounting(): Accounting + { + $tipjar = new Tipjar(); + $tipjar->setName('WALLET_TEST_TIPJAR'); + + $accounting = new Accounting(); + $accounting->setTipjar($tipjar); + + $this->entityManager->persist($tipjar); + $this->entityManager->flush(); + + return $accounting; + } + + public function testTransactionsAddFunds() + { + $tipjar = $this->getTipjarAccounting(); + $user = $this->getUserAccounting(); + + $balance = $this->walletService->calcBalance($user); + + $this->assertEquals(0, $balance->amount); + $this->assertEquals($user->getCurrency(), $balance->currency); + + $transaction = new AccountingTransaction(); + $transaction->setMoney(new Money(100, 'EUR')); + $transaction->setOrigin($tipjar); + $transaction->setTarget($user); + + $this->entityManager->persist($transaction); + $this->entityManager->flush(); + + $balance = $this->walletService->calcBalance($user); + + $this->assertEquals(100, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + } +} From 2df9a83de60c2eed269526e9b513a40d3a71236c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 10:57:46 +0200 Subject: [PATCH 32/86] Set emailConfirmed and active as false by default --- src/Entity/User.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Entity/User.php b/src/Entity/User.php index 078978b..ed9b8d2 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -151,6 +151,8 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs public function __construct() { + $this->emailConfirmed = false; + $this->active = false; $this->migrated = false; $this->tokens = new ArrayCollection(); From 6ff32e8c4e6abc3e3c322532060d55ed3e6e1127 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 10:58:20 +0200 Subject: [PATCH 33/86] Add balance to WalletStatement to hold available amount in statement --- src/Entity/WalletStatement.php | 15 +++++++++++++++ src/EventListener/WalletTransactionsListener.php | 1 + 2 files changed, 16 insertions(+) diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index 24f2e4f..4f33c15 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -24,6 +24,9 @@ class WalletStatement #[ORM\Column(enumType: WalletStatementDirection::class)] private ?WalletStatementDirection $direction = null; + #[ORM\Embedded(class: Money::class)] + private ?Money $balance = null; + /** * The Statements from which the money in the Transaction was drawed out of. * @@ -76,6 +79,18 @@ class WalletStatement return $this; } + public function getBalance(): ?Money + { + return $this->balance; + } + + public function setBalance(Money $balance): static + { + $this->balance = $balance; + + return $this; + } + /** * @return Collection */ diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index c47f0e3..3570a04 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -37,6 +37,7 @@ final class WalletTransactionsListener $statement = new WalletStatement(); $statement->setTransaction($transaction); $statement->setDirection(WalletStatementDirection::Incoming); + $statement->setBalance($transaction->getMoney()); $this->entityManager->persist($statement); $this->entityManager->flush(); From a574e271770fe8c56e9f9aa50363612e4468a740 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 10:58:46 +0200 Subject: [PATCH 34/86] Allow to calc great than or equal --- src/Library/Economy/MoneyService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/MoneyService.php b/src/Library/Economy/MoneyService.php index b416bad..e292b7b 100644 --- a/src/Library/Economy/MoneyService.php +++ b/src/Library/Economy/MoneyService.php @@ -64,11 +64,11 @@ class MoneyService * * @return bool `true` if `a` is more than `b` */ - public function isGreaterThan(Money $a, Money $b): bool + public function isGreaterThanOrEqual(Money $a, Money $b): bool { $a = $this->convert($a, $b->currency); - return $a->isGreaterThan(self::toBrick($b)); + return $a->isGreaterThanOrEqualTo(self::toBrick($b)); } private function convert(Money $money, string $toCurrency): Brick\Money From 4d3b7898d5db8d4e016523ee5dfc760e631e0825 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 11:53:51 +0200 Subject: [PATCH 35/86] Test for wallet save/spend behaviour --- .../Payment/WalletGatewayServiceTest.php | 105 ++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index 5f15a22..290aa61 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -7,6 +7,7 @@ use App\Entity\AccountingTransaction; use App\Entity\Money; use App\Entity\Tipjar; use App\Entity\User; +use App\Entity\WalletStatementDirection; use App\Library\Economy\Payment\WalletGatewayService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -63,22 +64,112 @@ class WalletGatewayServiceTest extends KernelTestCase $tipjar = $this->getTipjarAccounting(); $user = $this->getUserAccounting(); - $balance = $this->walletService->calcBalance($user); + $statements = $this->walletService->getStatements($user); + + $this->assertCount(0, $statements); + + $balance = $this->walletService->getBalance($user); $this->assertEquals(0, $balance->amount); $this->assertEquals($user->getCurrency(), $balance->currency); - $transaction = new AccountingTransaction(); - $transaction->setMoney(new Money(100, 'EUR')); - $transaction->setOrigin($tipjar); - $transaction->setTarget($user); + $incoming = new AccountingTransaction(); + $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); - $this->entityManager->persist($transaction); + $this->entityManager->persist($incoming); $this->entityManager->flush(); - $balance = $this->walletService->calcBalance($user); + /** + * $this->walletService->save($incoming); + * called automatically on Transaction persist + * @see \App\EventListener\WalletTransactionsListener + */ + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(1, $statements); + + $balance = $this->walletService->getBalance($user); $this->assertEquals(100, $balance->amount); $this->assertEquals('EUR', $balance->currency); } + + public function testTransactionsGetFinanced() + { + $tipjar = $this->getTipjarAccounting(); + $user = $this->getUserAccounting(); + + $incoming = new AccountingTransaction(); + $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $outgoing = new AccountingTransaction(); + $outgoing->setMoney(new Money(20, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(2, $statements); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(80, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $this->assertEquals(80, $statements[0]->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statements[0]->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(1, $statements[0]->getFinancesTo()); + $this->assertCount(0, $statements[0]->getFinancedBy()); + + $this->assertEquals(20, $statements[1]->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[1]->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statements[1]->getFinancesTo()); + $this->assertCount(1, $statements[1]->getFinancedBy()); + + $outgoing = new AccountingTransaction(); + $outgoing->setMoney(new Money(30, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(3, $statements); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(50, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $this->assertEquals(50, $statements[0]->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statements[0]->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(2, $statements[0]->getFinancesTo()); + $this->assertCount(0, $statements[0]->getFinancedBy()); + + $this->assertEquals(20, $statements[1]->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[1]->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statements[1]->getFinancesTo()); + $this->assertCount(1, $statements[1]->getFinancedBy()); + + $this->assertEquals(30, $statements[2]->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[2]->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statements[2]->getFinancesTo()); + $this->assertCount(1, $statements[2]->getFinancedBy()); + } } From 921e07e80c2853dad339c57c8b70347c97b18063 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 11:54:04 +0200 Subject: [PATCH 36/86] Use wallet service save method --- .../WalletTransactionsListener.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 3570a04..7135bf3 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -4,8 +4,7 @@ namespace App\EventListener; use App\Entity\AccountingTransaction; use App\Entity\User; -use App\Entity\WalletStatement; -use App\Entity\WalletStatementDirection; +use App\Library\Economy\Payment\WalletGatewayService; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; @@ -20,6 +19,7 @@ final class WalletTransactionsListener { public function __construct( private EntityManagerInterface $entityManager, + private WalletGatewayService $wallet ) {} /** @@ -30,16 +30,9 @@ final class WalletTransactionsListener PostPersistEventArgs $event, ) { $target = $transaction->getTarget(); - if ($target->getOwnerClass() !== User::class) { - return; + + if ($target->getOwnerClass() === User::class) { + $this->wallet->save($transaction); } - - $statement = new WalletStatement(); - $statement->setTransaction($transaction); - $statement->setDirection(WalletStatementDirection::Incoming); - $statement->setBalance($transaction->getMoney()); - - $this->entityManager->persist($statement); - $this->entityManager->flush(); } } From 73fc1fafc5111622da7d1e815671a3fb4f5b7313 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 12:26:35 +0200 Subject: [PATCH 37/86] Fix MoneyService API and add tests for it --- src/Library/Economy/MoneyService.php | 22 ++++---- tests/Library/Economy/MoneyServiceTest.php | 61 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 tests/Library/Economy/MoneyServiceTest.php diff --git a/src/Library/Economy/MoneyService.php b/src/Library/Economy/MoneyService.php index e292b7b..f2f3fe5 100644 --- a/src/Library/Economy/MoneyService.php +++ b/src/Library/Economy/MoneyService.php @@ -48,34 +48,34 @@ class MoneyService } /** - * Compares `a` to `b`. + * Compares if one Money is of less value than other. * - * @return bool `true` if `a` is less than `b` + * @return bool `true` if `$money` is less than `$than` */ - public function isLessThan(Money $a, Money $b): bool + public function isLess(Money $money, Money $than): bool { - $a = $this->convert($a, $b->currency); + $money = $this->convert($money, $than->currency); - return $a->isLessThan(self::toBrick($b)); + return $money->isLessThan(self::toBrick($than)); } /** - * Compaers `a` to `b`. + * Compaers if one Money is of greater or equal value to other. * - * @return bool `true` if `a` is more than `b` + * @return bool `true` if `$money` is more than `$than` */ - public function isGreaterThanOrEqual(Money $a, Money $b): bool + public function isMoreOrSame(Money $money, Money $than): bool { - $a = $this->convert($a, $b->currency); + $money = $this->convert($money, $than->currency); - return $a->isGreaterThanOrEqualTo(self::toBrick($b)); + return $money->isGreaterThanOrEqualTo(self::toBrick($than)); } private function convert(Money $money, string $toCurrency): Brick\Money { $fromCurrency = $money->currency; if ($fromCurrency === $toCurrency) { - return $money; + return self::toBrick($money); } $exchange = $this->exchangeLocator->getExchangeFor($fromCurrency, $toCurrency); diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php new file mode 100644 index 0000000..e9d0223 --- /dev/null +++ b/tests/Library/Economy/MoneyServiceTest.php @@ -0,0 +1,61 @@ +moneyService = static::getContainer()->get(MoneyService::class); + } + + public function testComparesLess() + { + $a = new Money(300, 'EUR'); + $b = new Money(200, 'EUR'); + + $this->assertTrue($this->moneyService->isLess($b, $a)); + $this->assertFalse($this->moneyService->isLess($a, $b)); + + $c = new Money(100, 'JPY'); + + // We are cooked if this fails and I'm not talking about code + $this->assertTrue($this->moneyService->isLess($c, $a)); + } + + public function testComparesMore() + { + $a = new Money(100, 'GBP'); + $b = new Money(100, 'GBP'); + + $this->assertTrue($this->moneyService->isMoreOrSame($a, $b)); + $this->assertTrue($this->moneyService->isMoreOrSame($b, $a)); + + $c = new Money(300, 'GBP'); + + $this->assertTrue($this->moneyService->isMoreOrSame($c, $b)); + $this->assertFalse($this->moneyService->isMoreOrSame($b, $c)); + + $d = new Money(100, 'MXN'); + + $this->assertFalse($this->moneyService->isMoreOrSame($d, $c)); + $this->assertFalse($this->moneyService->isMoreOrSame($d, $a)); + } +} From 6a4f97bc51562cdfd84fc6c842878537faeee7db Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 12:28:01 +0200 Subject: [PATCH 38/86] Pass WalletGatewayServiceTest --- .../Economy/Payment/WalletGatewayService.php | 99 ++++++++++++++++--- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Library/Economy/Payment/WalletGatewayService.php index 63fa5a8..398af09 100644 --- a/src/Library/Economy/Payment/WalletGatewayService.php +++ b/src/Library/Economy/Payment/WalletGatewayService.php @@ -3,42 +3,113 @@ namespace App\Library\Economy\Payment; use App\Entity\Accounting; +use App\Entity\AccountingTransaction; use App\Entity\Money; +use App\Entity\WalletStatement; use App\Entity\WalletStatementDirection; +use App\Library\Economy\MoneyService; use App\Repository\WalletStatementRepository; use Brick\Money as Brick; +use Doctrine\ORM\EntityManagerInterface; class WalletGatewayService { public function __construct( + private MoneyService $money, private WalletStatementRepository $walletStatementRepository, + private EntityManagerInterface $entityManager, ) {} - public function getAccountingBalance(Accounting $accounting): Money + /** + * @return \App\Entity\WalletStatement[] + */ + public function getStatements(Accounting $accounting): array { - $available = Brick\Money::ofMinor(0, $accounting->getCurrency()); - $statements = $this->walletStatementRepository->findByAccounting($accounting); + return $this->walletStatementRepository->findByAccounting($accounting); + } + + public function getBalance(Accounting $accounting): Money + { + $total = Brick\Money::ofMinor(0, $accounting->getCurrency()); + $statements = $this->getStatements($accounting); foreach ($statements as $statement) { - $transaction = $statement->getTransaction(); - $money = Brick\Money::ofMinor( - $transaction->getMoney()->amount, - $transaction->getMoney()->currency - ); + $transacted = $this->money->toBrick($statement->getTransaction()->getMoney()); switch ($statement->getDirection()) { case WalletStatementDirection::Incoming: - $available = $available->plus($money); + $total = $total->plus($transacted); break; case WalletStatementDirection::Outgoing: - $available = $available->minus($money); + $total = $total->minus($transacted); break; } } - return new Money( - $available->getMinorAmount()->toInt(), - $available->getCurrency()->getCurrencyCode() - ); + return $this->money->toMoney($total); + } + + /** + * Puts the money of a Transaction into the target wallet. + * + * @param AccountingTransaction $transaction The Transaction targetting a wallet + * @return \App\Entity\WalletStatement An incoming statement for the target wallet + */ + public function save(AccountingTransaction $transaction): WalletStatement + { + $statement = new WalletStatement(); + $statement->setTransaction($transaction); + $statement->setDirection(WalletStatementDirection::Incoming); + $statement->setBalance($transaction->getMoney()); + + $this->entityManager->persist($statement); + $this->entityManager->flush(); + + return $statement; + } + + /** + * Takes the money of a Transaction from the origin wallet. + * + * @param AccountingTransaction $transaction The Transaction originating from a wallet + * @return \App\Entity\WalletStatement An outgoing statement financed by previous incoming statements + */ + public function spend(AccountingTransaction $transaction): WalletStatement + { + $origin = $transaction->getOrigin(); + $toSpend = $transaction->getMoney(); + + $outgoing = new WalletStatement(); + $outgoing->setTransaction($transaction); + $outgoing->setDirection(WalletStatementDirection::Outgoing); + + $incomings = $this->getStatements($origin); + foreach ($incomings as $incoming) { + $spent = new Money(0, $toSpend->currency); + $balance = $incoming->getBalance(); + + if ($balance->amount === 0) { + continue; + } + + if ($this->money->isMoreOrSame($balance, $spent)) { + $spent = $toSpend; + } else { + $spent = $this->money->add($balance, $spent); + } + + $balance = $this->money->substract($spent, $balance); + + $incoming->setBalance($balance); + + $outgoing->setBalance($spent); + $outgoing->addFinancedBy($incoming); + + $this->entityManager->persist($incoming); + $this->entityManager->persist($outgoing); + $this->entityManager->flush(); + + return $outgoing; + } } } From 8b52c125ff1ea36221445b8e04852948dbcfb9e8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 12:29:22 +0200 Subject: [PATCH 39/86] Style fixes by CS Fixer --- src/EventListener/WalletTransactionsListener.php | 2 +- src/Library/Economy/Payment/WalletGatewayService.php | 10 ++++++---- tests/Library/Economy/MoneyServiceTest.php | 4 ++-- .../Economy/Payment/WalletGatewayServiceTest.php | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 7135bf3..7d7402b 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -19,7 +19,7 @@ final class WalletTransactionsListener { public function __construct( private EntityManagerInterface $entityManager, - private WalletGatewayService $wallet + private WalletGatewayService $wallet, ) {} /** diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Library/Economy/Payment/WalletGatewayService.php index 398af09..3556447 100644 --- a/src/Library/Economy/Payment/WalletGatewayService.php +++ b/src/Library/Economy/Payment/WalletGatewayService.php @@ -51,9 +51,10 @@ class WalletGatewayService /** * Puts the money of a Transaction into the target wallet. - * + * * @param AccountingTransaction $transaction The Transaction targetting a wallet - * @return \App\Entity\WalletStatement An incoming statement for the target wallet + * + * @return WalletStatement An incoming statement for the target wallet */ public function save(AccountingTransaction $transaction): WalletStatement { @@ -70,9 +71,10 @@ class WalletGatewayService /** * Takes the money of a Transaction from the origin wallet. - * + * * @param AccountingTransaction $transaction The Transaction originating from a wallet - * @return \App\Entity\WalletStatement An outgoing statement financed by previous incoming statements + * + * @return WalletStatement An outgoing statement financed by previous incoming statements */ public function spend(AccountingTransaction $transaction): WalletStatement { diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php index e9d0223..949f4ca 100644 --- a/tests/Library/Economy/MoneyServiceTest.php +++ b/tests/Library/Economy/MoneyServiceTest.php @@ -9,7 +9,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** * WARNING: This test compares currencies, which change over time * and might cause the test to fail without justifiable cause in the code. - * + * * Test currencies and values have been selected for their stability to minimize chances of external factors such as * * Hyper inflation/deflation * * Currency intervention @@ -47,7 +47,7 @@ class MoneyServiceTest extends KernelTestCase $this->assertTrue($this->moneyService->isMoreOrSame($a, $b)); $this->assertTrue($this->moneyService->isMoreOrSame($b, $a)); - + $c = new Money(300, 'GBP'); $this->assertTrue($this->moneyService->isMoreOrSame($c, $b)); diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index 290aa61..6dec582 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -83,10 +83,10 @@ class WalletGatewayServiceTest extends KernelTestCase /** * $this->walletService->save($incoming); - * called automatically on Transaction persist + * called automatically on Transaction persist. + * * @see \App\EventListener\WalletTransactionsListener */ - $statements = $this->walletService->getStatements($user); $this->assertCount(1, $statements); From 739bb2691f98201bce98a6e667b4788cfcc90b7a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 14:25:29 +0200 Subject: [PATCH 40/86] Add ApiResource entities --- src/Entity/Interface/AccountingOwnerInterface.php | 14 ++++++++++++++ src/Entity/Interface/ApiResource.php | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/Entity/Interface/AccountingOwnerInterface.php create mode 100644 src/Entity/Interface/ApiResource.php diff --git a/src/Entity/Interface/AccountingOwnerInterface.php b/src/Entity/Interface/AccountingOwnerInterface.php new file mode 100644 index 0000000..d74c97e --- /dev/null +++ b/src/Entity/Interface/AccountingOwnerInterface.php @@ -0,0 +1,14 @@ + Date: Fri, 25 Oct 2024 14:36:02 +0200 Subject: [PATCH 41/86] Update accounting entity ahead of being api resrouce --- src/Entity/Accounting.php | 110 +++++------------- src/Entity/Project.php | 6 +- src/Entity/Tipjar.php | 6 +- src/Entity/User.php | 6 +- src/EventListener/AccountingOwnerListener.php | 44 +++++++ src/Library/Benzina/Pump/ProjectsPump.php | 4 - src/Library/Benzina/Pump/UsersPump.php | 5 - tests/Entity/AccountingTest.php | 8 +- .../Payment/WalletGatewayServiceTest.php | 23 ++-- 9 files changed, 96 insertions(+), 116 deletions(-) create mode 100644 src/EventListener/AccountingOwnerListener.php diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index 691d37a..504ad08 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -2,9 +2,8 @@ namespace App\Entity; -use ApiPlatform\Metadata as API; +use App\Entity\Interface\AccountingOwnerInterface; use App\Repository\AccountingRepository; -use App\Service\ApiResourceNormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -12,10 +11,10 @@ use Symfony\Component\Validator\Constraints as Assert; * Accountings are the receivers and the issuers of Transactions. */ #[ORM\Entity(repositoryClass: AccountingRepository::class)] -#[API\Get()] -#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')] class Accounting { + public const OWNER_CHANGE_NOT_ALLOWED = 'Are you trying to commit fraud? Cannot change ownership of an Accounting.'; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -29,22 +28,12 @@ class Accounting #[ORM\Column(length: 3)] private ?string $currency = null; - #[API\ApiProperty(writable: false, readable: false)] + #[ORM\Column] + private ?int $ownerId = null; + #[ORM\Column(length: 255)] private ?string $ownerClass = null; - #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])] - private ?User $user = null; - - #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])] - private ?Project $project = null; - - #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])] - private ?Tipjar $tipjar = null; - public function __construct() { /* @@ -71,19 +60,39 @@ class Accounting return $this; } + public function getOwnerId(): ?int + { + return $this->ownerId; + } + + private function setOwnerId(?int $ownerId): static + { + // ensure ownership does not change + if ( + $this->ownerId !== null + && $this->ownerId !== $ownerId + ) { + throw new \Exception(self::OWNER_CHANGE_NOT_ALLOWED); + } + + $this->ownerId = $ownerId; + + return $this; + } + public function getOwnerClass(): ?string { return $this->ownerClass; } - public function setOwnerClass(string $ownerClass): static + private function setOwnerClass(string $ownerClass): static { // ensure ownership does not change if ( $this->ownerClass !== null && $this->ownerClass !== $ownerClass ) { - throw new \Exception('Are you trying to commit fraud? Cannot change ownership of an Accounting.'); + throw new \Exception(self::OWNER_CHANGE_NOT_ALLOWED); } $this->ownerClass = $ownerClass; @@ -91,67 +100,10 @@ class Accounting return $this; } - public function getOwnerResource(): string + public function setOwner(AccountingOwnerInterface $owner): static { - return ApiResourceNormalizer::toResource($this->ownerClass); - } - - public function getUser(): ?User - { - return $this->user; - } - - public function setUser(User $user): static - { - // set the owning side of the relation if necessary - if ($user->getAccounting() !== $this) { - $user->setAccounting($this); - } - - // set the owner class - $this->setOwnerClass($user::class); - - $this->user = $user; - - return $this; - } - - public function getProject(): ?Project - { - return $this->project; - } - - public function setProject(Project $project): static - { - // set the owning side of the relation if necessary - if ($project->getAccounting() !== $this) { - $project->setAccounting($this); - } - - // set the owner class - $this->setOwnerClass($project::class); - - $this->project = $project; - - return $this; - } - - public function getTipjar(): ?Tipjar - { - return $this->tipjar; - } - - public function setTipjar(Tipjar $tipjar): static - { - // set the owning side of the relation if necessary - if ($tipjar->getAccounting() !== $this) { - $tipjar->setAccounting($this); - } - - // set the owner class - $this->setOwnerClass($tipjar::class); - - $this->tipjar = $tipjar; + $this->setOwnerId($owner->getId()); + $this->setOwnerClass($owner::class); return $this; } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 09fb57d..0e932d6 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -4,6 +4,7 @@ namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; +use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Trait\TimestampableCreationEntity; use App\Entity\Trait\TimestampableUpdationEntity; use App\Repository\ProjectRepository; @@ -23,7 +24,7 @@ use Doctrine\ORM\Mapping as ORM; 'owner' => 'exact', ])] #[ORM\Entity(repositoryClass: ProjectRepository::class)] -class Project +class Project implements AccountingOwnerInterface { use TimestampableCreationEntity; use TimestampableUpdationEntity; @@ -44,8 +45,7 @@ class Project * A Project's Accounting represents how much money the Project has raised from the community. */ #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(inversedBy: 'project', cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[ORM\OneToOne(cascade: ['persist'])] private ?Accounting $accounting = null; /** diff --git a/src/Entity/Tipjar.php b/src/Entity/Tipjar.php index 2f0a00f..3d08988 100644 --- a/src/Entity/Tipjar.php +++ b/src/Entity/Tipjar.php @@ -3,6 +3,7 @@ namespace App\Entity; use ApiPlatform\Metadata as API; +use App\Entity\Interface\AccountingOwnerInterface; use App\Repository\TipjarRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -19,7 +20,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; #[API\Delete()] #[API\Patch()] #[ORM\Entity(repositoryClass: TipjarRepository::class)] -class Tipjar +class Tipjar implements AccountingOwnerInterface { #[ORM\Id] #[ORM\GeneratedValue] @@ -33,8 +34,7 @@ class Tipjar private ?string $name = null; #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[ORM\OneToOne(cascade: ['persist'])] private ?Accounting $accounting = null; public function getId(): ?int diff --git a/src/Entity/User.php b/src/Entity/User.php index ed9b8d2..69e81e5 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,7 @@ namespace App\Entity; use ApiPlatform\Metadata as API; +use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\UserOwnedInterface; use App\Entity\Trait\TimestampableCreationEntity; use App\Entity\Trait\TimestampableUpdationEntity; @@ -38,7 +39,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Index(fields: ['migratedId'])] -class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface +class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { use TimestampableCreationEntity; use TimestampableUpdationEntity; @@ -95,8 +96,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs private ?bool $emailConfirmed = null; #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[ORM\OneToOne(cascade: ['persist'])] private ?Accounting $accounting = null; /** diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php new file mode 100644 index 0000000..8359fdd --- /dev/null +++ b/src/EventListener/AccountingOwnerListener.php @@ -0,0 +1,44 @@ +getObject(); + + if (!$entity instanceof AccountingOwnerInterface) { + return; + } + + $accounting = $entity->getAccounting(); + if ($accounting === null || $accounting->getId() === null) { + $accounting = new Accounting(); + $accounting->setOwner($entity); + + $this->entityManager->persist($accounting); + $this->entityManager->flush(); + + $entity->setAccounting($accounting); + } + } +} diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index cb31167..b2ccf1c 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -132,11 +132,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface $project->setDateCreated(new \DateTime($record['created'])); $project->setDateUpdated(new \DateTime()); - $accounting = $this->getAccounting($record); - $accounting->setProject($project); - $this->entityManager->persist($project); - $this->entityManager->persist($accounting); } $this->entityManager->flush(); diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 9e42a6b..69d74bc 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -69,7 +69,6 @@ class UsersPump extends AbstractPump implements PumpInterface foreach ($batch as $key => $record) { $user = new User(); - $user->setAccounting(new Accounting()); $user->setUsername($this->getUsername($record)); $user->setPassword($record['password'] ?? ''); $user->setEmail($record['email']); @@ -79,11 +78,7 @@ class UsersPump extends AbstractPump implements PumpInterface $user->setMigrated(true); $user->setMigratedId($record['id']); - $accounting = new Accounting(); - $accounting->setUser($user); - $this->entityManager->persist($user); - $this->entityManager->persist($accounting); } $this->entityManager->flush(); diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php index df69e3b..89aa398 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/AccountingTest.php @@ -27,7 +27,7 @@ class AccountingTest extends KernelTestCase $tipjarA->setName('TEST_TIPJAR_A'); $accountingA = new Accounting(); - $accountingA->setTipjar($tipjarA); + $accountingA->setOwner($tipjarA); $this->entityManager->persist($tipjarA); $this->entityManager->flush(); @@ -38,14 +38,14 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingA->getId()); + $this->assertEquals($tipjarA->getId(), $accountingA->getOwnerId()); $this->assertEquals(Tipjar::class, $accountingA->getOwnerClass()); - $this->assertEquals('TEST_TIPJAR_A', $accountingA->getTipjar()->getName()); $tipjarB = new Tipjar(); $tipjarB->setName('TEST_TIPJAR_B'); $accountingB = new Accounting(); - $accountingB->setTipjar($tipjarB); + $accountingB->setOwner($tipjarB); $this->entityManager->persist($tipjarB); $this->entityManager->flush(); @@ -56,7 +56,7 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingB->getId()); + $this->assertEquals($tipjarB->getId(), $accountingB->getOwnerId()); $this->assertEquals(Tipjar::class, $accountingB->getOwnerClass()); - $this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName()); } } diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index 6dec582..b6db8da 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -28,41 +28,34 @@ class WalletGatewayServiceTest extends KernelTestCase $this->walletService = static::getContainer()->get(WalletGatewayService::class); } - private function getUserAccounting(): Accounting + private function getUser(): User { $user = new User(); $user->setUsername('wallettestuser'); $user->setEmail('testuser@wallet.com'); $user->setPassword('wallettestpassword'); - $accounting = new Accounting(); - $accounting->setCurrency('EUR'); - $accounting->setUser($user); - $this->entityManager->persist($user); $this->entityManager->flush(); - return $accounting; + return $user; } - private function getTipjarAccounting(): Accounting + private function getTipjar(): Tipjar { $tipjar = new Tipjar(); $tipjar->setName('WALLET_TEST_TIPJAR'); - $accounting = new Accounting(); - $accounting->setTipjar($tipjar); - $this->entityManager->persist($tipjar); $this->entityManager->flush(); - return $accounting; + return $tipjar; } public function testTransactionsAddFunds() { - $tipjar = $this->getTipjarAccounting(); - $user = $this->getUserAccounting(); + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); $statements = $this->walletService->getStatements($user); @@ -99,8 +92,8 @@ class WalletGatewayServiceTest extends KernelTestCase public function testTransactionsGetFinanced() { - $tipjar = $this->getTipjarAccounting(); - $user = $this->getUserAccounting(); + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); $incoming = new AccountingTransaction(); $incoming->setMoney(new Money(100, 'EUR')); From 626c09bdab25af8a5601056fcc724fe9f9a91696 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 14:36:21 +0200 Subject: [PATCH 42/86] Expose custom api resource for accounting --- src/ApiResource/AccountingApiResource.php | 58 +++++++++++++++++++++++ src/State/AccountingStateProcessor.php | 33 +++++++++++++ src/State/AccountingStateProvider.php | 48 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 src/ApiResource/AccountingApiResource.php create mode 100644 src/State/AccountingStateProcessor.php create mode 100644 src/State/AccountingStateProvider.php diff --git a/src/ApiResource/AccountingApiResource.php b/src/ApiResource/AccountingApiResource.php new file mode 100644 index 0000000..ade3b82 --- /dev/null +++ b/src/ApiResource/AccountingApiResource.php @@ -0,0 +1,58 @@ +accounting->getId(); + } + + public function getCurrency(): ?string + { + return $this->accounting->getCurrency(); + } + + public function setCurrency(string $currency): static + { + $this->accounting->setCurrency($currency); + + return $this; + } + + public function getOwner(): ApiResource + { + return $this->owner; + } +} diff --git a/src/State/AccountingStateProcessor.php b/src/State/AccountingStateProcessor.php new file mode 100644 index 0000000..6e7b278 --- /dev/null +++ b/src/State/AccountingStateProcessor.php @@ -0,0 +1,33 @@ +entityManager->find(Accounting::class, $data->getId()); + $accounting->setCurrency($data->getCurrency()); + + $this->entityManager->persist($accounting); + $this->entityManager->flush(); + + $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); + + return new AccountingApiResource($accounting, $owner); + } +} diff --git a/src/State/AccountingStateProvider.php b/src/State/AccountingStateProvider.php new file mode 100644 index 0000000..228f350 --- /dev/null +++ b/src/State/AccountingStateProvider.php @@ -0,0 +1,48 @@ +collectionProvider->provide($operation, $uriVariables, $context); + + foreach ($accountings as $key => $accounting) { + $accountings[$key] = $this->toResource($accounting); + } + + return $accountings; + } + + $accounting = $this->itemProvider->provide($operation, $uriVariables, $context); + + return $this->toResource($accounting); + } + + private function toResource(Accounting $accounting): AccountingApiResource + { + $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); + + return new AccountingApiResource($accounting, $owner); + } +} From 103ae437e0f7474862463f166ce5ff6d3b589870 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 25 Oct 2024 14:41:23 +0200 Subject: [PATCH 43/86] Fix accountings state provider --- src/State/AccountingStateProvider.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/State/AccountingStateProvider.php b/src/State/AccountingStateProvider.php index 228f350..7aeb748 100644 --- a/src/State/AccountingStateProvider.php +++ b/src/State/AccountingStateProvider.php @@ -6,6 +6,7 @@ use ApiPlatform\Doctrine\Orm\State\CollectionProvider; use ApiPlatform\Doctrine\Orm\State\ItemProvider; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Pagination\TraversablePaginator; use ApiPlatform\State\ProviderInterface; use App\ApiResource\AccountingApiResource; use App\Entity\Accounting; @@ -27,11 +28,17 @@ class AccountingStateProvider implements ProviderInterface if ($operation instanceof CollectionOperationInterface) { $accountings = $this->collectionProvider->provide($operation, $uriVariables, $context); - foreach ($accountings as $key => $accounting) { - $accountings[$key] = $this->toResource($accounting); + $resources = []; + foreach ($accountings as $accounting) { + $resources[] = $this->toResource($accounting); } - return $accountings; + return new TraversablePaginator( + new \ArrayIterator($resources), + $accountings->getCurrentPage(), + $accountings->getItemsPerPage(), + $accountings->getTotalItems() + ); } $accounting = $this->itemProvider->provide($operation, $uriVariables, $context); @@ -39,8 +46,12 @@ class AccountingStateProvider implements ProviderInterface return $this->toResource($accounting); } - private function toResource(Accounting $accounting): AccountingApiResource + private function toResource(?Accounting $accounting): ?AccountingApiResource { + if ($accounting === null) { + return null; + } + $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); return new AccountingApiResource($accounting, $owner); From f50d0352e100ca7fbfa19057f42c85db2b744903 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 12:54:52 +0100 Subject: [PATCH 44/86] Fix typos --- src/Library/Economy/MoneyService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/MoneyService.php b/src/Library/Economy/MoneyService.php index f2f3fe5..c6649fc 100644 --- a/src/Library/Economy/MoneyService.php +++ b/src/Library/Economy/MoneyService.php @@ -60,9 +60,9 @@ class MoneyService } /** - * Compaers if one Money is of greater or equal value to other. + * Compares if one Money is of greater or equal value than other. * - * @return bool `true` if `$money` is more than `$than` + * @return bool `true` if `$money` is more than or same as `$than` */ public function isMoreOrSame(Money $money, Money $than): bool { From 0397912cadf349b2c2cde8d0ba38f35c75e933ec Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 13:09:53 +0100 Subject: [PATCH 45/86] Fix users dates pumping --- src/Library/Benzina/Pump/UsersPump.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 69d74bc..4764e66 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -68,6 +68,11 @@ class UsersPump extends AbstractPump implements PumpInterface $batch = $this->skipPumped($batch, 'id', User::class, 'migratedId'); foreach ($batch as $key => $record) { + $created = new \DateTime($record['created']); + if ($created < new \DateTime('2000-01-01')) { + $created = new \DateTime($record['modified']); + } + $user = new User(); $user->setUsername($this->getUsername($record)); $user->setPassword($record['password'] ?? ''); @@ -77,6 +82,8 @@ class UsersPump extends AbstractPump implements PumpInterface $user->setActive(false); $user->setMigrated(true); $user->setMigratedId($record['id']); + $user->setDateCreated($created); + $user->setDateUpdated(new \DateTime()); $this->entityManager->persist($user); } From 474042f0f4d7866e0c8138d2d5fd4f769411f68f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 13:43:01 +0100 Subject: [PATCH 46/86] Update PumpInterface to get config key value instead of config array at key --- src/Library/Benzina/Pump/AbstractPump.php | 10 ++++------ src/Library/Benzina/Pump/PumpInterface.php | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Library/Benzina/Pump/AbstractPump.php b/src/Library/Benzina/Pump/AbstractPump.php index 0a2ee97..3775fe7 100644 --- a/src/Library/Benzina/Pump/AbstractPump.php +++ b/src/Library/Benzina/Pump/AbstractPump.php @@ -11,14 +11,12 @@ abstract class AbstractPump implements PumpInterface $this->config = $config; } - public function getConfig(?string $key = null, mixed $default = null): array + public function getConfig(?string $key = null, mixed $default = null): mixed { if ($key !== null) { - return [ - $key => \array_key_exists($key, $this->config) - ? $this->config[$key] - : $default, - ]; + return \array_key_exists($key, $this->config) + ? $this->config[$key] + : $default; } return $this->config; diff --git a/src/Library/Benzina/Pump/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index 8f84aa8..7dbb670 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -17,9 +17,9 @@ interface PumpInterface * @param string|null $key A configuration array key to return * @param mixed|null $default Any value to return in case the configuration key does not exist * - * @return array The configuration array at the specified key, or all keys if null + * @return array The configuration value at the specified key, or full keys and values if null */ - public function getConfig(?string $key = null, mixed $default = null): array; + public function getConfig(?string $key = null, mixed $default = null): mixed; /** * Determines if a data batch is supported by this pump. From 3683146f3f99879c0502786a6aa8bfbc37e56daf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 13:43:38 +0100 Subject: [PATCH 47/86] Simplify skip pumped by default --- src/Command/BenzinaPumpCommand.php | 2 +- src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index ad717a3..5fe6179 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -52,7 +52,7 @@ class BenzinaPumpCommand extends Command null, InputOption::VALUE_NEGATABLE, 'Skips feeding already pumped records in a batch', - false + true ); $this->addUsage('app:benzina:pump --no-debug user'); diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php index f7a86a7..61e2e78 100644 --- a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php +++ b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php @@ -31,7 +31,7 @@ trait DoctrinePumpTrait string $entityClass, string $entityKey, ): array { - if (!$this->getConfig('skip-pumped', false)) { + if (!$this->getConfig('skip-pumped')) { return $batch; } From 554bcc5cb3d21766847ed003178693bda99d29ef Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 13:44:35 +0100 Subject: [PATCH 48/86] Code improvements --- src/ApiResource/AccountingApiResource.php | 2 +- src/EventListener/AccountingOwnerListener.php | 2 +- src/Library/Benzina/Pump/UsersPump.php | 1 - src/State/AccountingStateProcessor.php | 4 ++-- tests/Library/Economy/Payment/WalletGatewayServiceTest.php | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ApiResource/AccountingApiResource.php b/src/ApiResource/AccountingApiResource.php index ade3b82..44d4f90 100644 --- a/src/ApiResource/AccountingApiResource.php +++ b/src/ApiResource/AccountingApiResource.php @@ -31,7 +31,7 @@ class AccountingApiResource { public function __construct( private readonly Accounting $accounting, - private readonly AccountingOwnerInterface $owner + private readonly AccountingOwnerInterface $owner, ) {} public function getId(): ?int diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php index 8359fdd..91a58fb 100644 --- a/src/EventListener/AccountingOwnerListener.php +++ b/src/EventListener/AccountingOwnerListener.php @@ -22,7 +22,7 @@ final class AccountingOwnerListener * Generates a WalletStatement for User-received Transactions. */ public function postPersist( - PostPersistEventArgs $event + PostPersistEventArgs $event, ) { $entity = $event->getObject(); diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 4764e66..ae5f8d7 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -2,7 +2,6 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; use App\Entity\User; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; diff --git a/src/State/AccountingStateProcessor.php b/src/State/AccountingStateProcessor.php index 6e7b278..0e4c8c4 100644 --- a/src/State/AccountingStateProcessor.php +++ b/src/State/AccountingStateProcessor.php @@ -15,11 +15,11 @@ class AccountingStateProcessor implements ProcessorInterface ) {} /** - * @param \App\ApiResource\AccountingApiResource $data + * @param AccountingApiResource $data */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - /** @var \App\Entity\Accounting */ + /** @var Accounting */ $accounting = $this->entityManager->find(Accounting::class, $data->getId()); $accounting->setCurrency($data->getCurrency()); diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index b6db8da..06ddf37 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Library\Economy\Payment; -use App\Entity\Accounting; use App\Entity\AccountingTransaction; use App\Entity\Money; use App\Entity\Tipjar; From 7677f83c4d856897d5a8dd4c933618f7f7200e12 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 14:08:10 +0100 Subject: [PATCH 49/86] Set accounting in owner side --- src/EventListener/AccountingOwnerListener.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php index 91a58fb..1994beb 100644 --- a/src/EventListener/AccountingOwnerListener.php +++ b/src/EventListener/AccountingOwnerListener.php @@ -39,6 +39,9 @@ final class AccountingOwnerListener $this->entityManager->flush(); $entity->setAccounting($accounting); + + $this->entityManager->persist($entity); + $this->entityManager->flush(); } } } From 9bdeccb38135b42e08e4f8be6e68f456be85eaa5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 15:00:03 +0100 Subject: [PATCH 50/86] Rename ApiResourceNormalizer to generic ApiService --- src/ApiResource/Version.php | 4 ++-- src/DependencyInjection/Compiler/VersionedResourcePass.php | 4 ++-- src/Service/{ApiResourceNormalizer.php => ApiService.php} | 5 +---- 3 files changed, 5 insertions(+), 8 deletions(-) rename src/Service/{ApiResourceNormalizer.php => ApiService.php} (83%) diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php index 71e6437..a04432f 100644 --- a/src/ApiResource/Version.php +++ b/src/ApiResource/Version.php @@ -5,7 +5,7 @@ namespace App\ApiResource; use ApiPlatform\Metadata as API; use App\Filter\ResourceVersionResourceFilter; use App\Filter\ResourceVersionResourceIdFilter; -use App\Service\ApiResourceNormalizer; +use App\Service\ApiService; use App\State\ResourceVersionStateProvider; use Gedmo\Loggable\Entity\LogEntry; @@ -55,7 +55,7 @@ class Version */ public function getResource(): string { - return ApiResourceNormalizer::toResource($this->log->getObjectClass()); + return ApiService::toResource($this->log->getObjectClass()); } /** diff --git a/src/DependencyInjection/Compiler/VersionedResourcePass.php b/src/DependencyInjection/Compiler/VersionedResourcePass.php index 2d67ac7..5f8e9c2 100644 --- a/src/DependencyInjection/Compiler/VersionedResourcePass.php +++ b/src/DependencyInjection/Compiler/VersionedResourcePass.php @@ -2,7 +2,7 @@ namespace App\DependencyInjection\Compiler; -use App\Service\ApiResourceNormalizer; +use App\Service\ApiService; use App\Service\VersionedResourceService; use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -28,7 +28,7 @@ class VersionedResourcePass implements CompilerPassInterface $versionedResourceNames = [ ...$versionedResourceNames, - ApiResourceNormalizer::toResource($entityClass), + ApiService::toResource($entityClass), ]; } diff --git a/src/Service/ApiResourceNormalizer.php b/src/Service/ApiService.php similarity index 83% rename from src/Service/ApiResourceNormalizer.php rename to src/Service/ApiService.php index ad43e97..3ac8503 100644 --- a/src/Service/ApiResourceNormalizer.php +++ b/src/Service/ApiService.php @@ -2,10 +2,7 @@ namespace App\Service; -/** - * Normalizes between Entity classnames and API resource names. - */ -class ApiResourceNormalizer +class ApiService { /** * Converts an Entity class name into an API resource name. From d2ab2d98fe1f62f75020fdf386d286539d14a3ab Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 28 Oct 2024 15:09:44 +0100 Subject: [PATCH 51/86] Avoid double persisting entity --- src/EventListener/AccountingOwnerListener.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php index 1994beb..8800aa5 100644 --- a/src/EventListener/AccountingOwnerListener.php +++ b/src/EventListener/AccountingOwnerListener.php @@ -35,9 +35,6 @@ final class AccountingOwnerListener $accounting = new Accounting(); $accounting->setOwner($entity); - $this->entityManager->persist($accounting); - $this->entityManager->flush(); - $entity->setAccounting($accounting); $this->entityManager->persist($entity); From 2a59bdd4f54ab4df032ed999e2c1a47d396f952f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 12:33:26 +0100 Subject: [PATCH 52/86] Test that wallet statements are FIFO --- .../Payment/WalletGatewayServiceTest.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index 06ddf37..1086428 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -164,4 +164,78 @@ class WalletGatewayServiceTest extends KernelTestCase $this->assertCount(0, $statements[2]->getFinancesTo()); $this->assertCount(1, $statements[2]->getFinancedBy()); } + + public function testBalanceIsFIFO() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $incoming = new AccountingTransaction(); + $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $incoming = new AccountingTransaction(); + $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $outgoing = new AccountingTransaction(); + $outgoing->setMoney(new Money(15, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $incoming = new AccountingTransaction(); + $incoming->setMoney(new Money(5, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(10, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(4, $statements); + + $statement = $statements[0]; + $this->assertEquals(0, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(1, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + + $statement = $statements[1]; + $this->assertEquals(5, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(1, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + + $statement = $statements[2]; + $this->assertEquals(15, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(2, $statement->getFinancedBy()); + + $statement = $statements[3]; + $this->assertEquals(5, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + } } From 4cc9c97ff1f12895de05794973577475c2005b07 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 12:40:32 +0100 Subject: [PATCH 53/86] Improve WalletGatewayServiceTest code --- .../Payment/WalletGatewayServiceTest.php | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php index 1086428..9725fb2 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Library/Economy/Payment/WalletGatewayServiceTest.php @@ -70,23 +70,30 @@ class WalletGatewayServiceTest extends KernelTestCase $incoming->setOrigin($tipjar); $incoming->setTarget($user); - $this->entityManager->persist($incoming); - $this->entityManager->flush(); - - /** + /* * $this->walletService->save($incoming); * called automatically on Transaction persist. * * @see \App\EventListener\WalletTransactionsListener */ - $statements = $this->walletService->getStatements($user); - - $this->assertCount(1, $statements); + $this->entityManager->persist($incoming); + $this->entityManager->flush(); $balance = $this->walletService->getBalance($user); $this->assertEquals(100, $balance->amount); $this->assertEquals('EUR', $balance->currency); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(1, $statements); + + $statement = $statements[0]; + $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); } public function testTransactionsGetFinanced() @@ -109,26 +116,28 @@ class WalletGatewayServiceTest extends KernelTestCase $this->walletService->spend($outgoing); - $statements = $this->walletService->getStatements($user); - - $this->assertCount(2, $statements); - $balance = $this->walletService->getBalance($user); $this->assertEquals(80, $balance->amount); $this->assertEquals('EUR', $balance->currency); - $this->assertEquals(80, $statements[0]->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statements[0]->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); - $this->assertCount(1, $statements[0]->getFinancesTo()); - $this->assertCount(0, $statements[0]->getFinancedBy()); + $statements = $this->walletService->getStatements($user); - $this->assertEquals(20, $statements[1]->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[1]->getDirection()->value); + $this->assertCount(2, $statements); + + $statement = $statements[0]; + $this->assertEquals(80, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); - $this->assertCount(0, $statements[1]->getFinancesTo()); - $this->assertCount(1, $statements[1]->getFinancedBy()); + $this->assertCount(1, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + + $statement = $statements[1]; + $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(1, $statement->getFinancedBy()); $outgoing = new AccountingTransaction(); $outgoing->setMoney(new Money(30, 'EUR')); @@ -137,32 +146,35 @@ class WalletGatewayServiceTest extends KernelTestCase $this->walletService->spend($outgoing); - $statements = $this->walletService->getStatements($user); - - $this->assertCount(3, $statements); - $balance = $this->walletService->getBalance($user); $this->assertEquals(50, $balance->amount); $this->assertEquals('EUR', $balance->currency); - $this->assertEquals(50, $statements[0]->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statements[0]->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); - $this->assertCount(2, $statements[0]->getFinancesTo()); - $this->assertCount(0, $statements[0]->getFinancedBy()); + $statements = $this->walletService->getStatements($user); - $this->assertEquals(20, $statements[1]->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[1]->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); - $this->assertCount(0, $statements[1]->getFinancesTo()); - $this->assertCount(1, $statements[1]->getFinancedBy()); + $this->assertCount(3, $statements); - $this->assertEquals(30, $statements[2]->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statements[2]->getDirection()->value); + $statement = $statements[0]; + $this->assertEquals(50, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); - $this->assertCount(0, $statements[2]->getFinancesTo()); - $this->assertCount(1, $statements[2]->getFinancedBy()); + $this->assertCount(2, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + + $statement = $statements[1]; + $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(1, $statement->getFinancedBy()); + + $statement = $statements[2]; + $this->assertEquals(30, $statement->getBalance()->amount); + $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(1, $statement->getFinancedBy()); } public function testBalanceIsFIFO() From aedef561a628d3c9ed9d2babe2981e3adca9ca5a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 12:41:13 +0100 Subject: [PATCH 54/86] Add return type annotation --- src/State/AccountingStateProcessor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/State/AccountingStateProcessor.php b/src/State/AccountingStateProcessor.php index 0e4c8c4..f2e65c7 100644 --- a/src/State/AccountingStateProcessor.php +++ b/src/State/AccountingStateProcessor.php @@ -16,6 +16,8 @@ class AccountingStateProcessor implements ProcessorInterface /** * @param AccountingApiResource $data + * + * @return AccountingApiResource */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { From 9f931a2626f6b56ad7c8756bb00186eeb99349ff Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 12:41:52 +0100 Subject: [PATCH 55/86] Fix Wallet spend calculations --- .../Economy/Payment/WalletGatewayService.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Library/Economy/Payment/WalletGatewayService.php index 3556447..4cec990 100644 --- a/src/Library/Economy/Payment/WalletGatewayService.php +++ b/src/Library/Economy/Payment/WalletGatewayService.php @@ -79,7 +79,9 @@ class WalletGatewayService public function spend(AccountingTransaction $transaction): WalletStatement { $origin = $transaction->getOrigin(); - $toSpend = $transaction->getMoney(); + + $spendGoal = $transaction->getMoney(); + $spentTotal = new Money(0, $spendGoal->currency); $outgoing = new WalletStatement(); $outgoing->setTransaction($transaction); @@ -87,31 +89,36 @@ class WalletGatewayService $incomings = $this->getStatements($origin); foreach ($incomings as $incoming) { - $spent = new Money(0, $toSpend->currency); + if ($spendGoal->amount === 0) { + break; + } + $balance = $incoming->getBalance(); if ($balance->amount === 0) { continue; } - if ($this->money->isMoreOrSame($balance, $spent)) { - $spent = $toSpend; + if ($this->money->isLess($spendGoal, $balance)) { + $balanceSpent = $spendGoal; } else { - $spent = $this->money->add($balance, $spent); + $balanceSpent = $balance; } - $balance = $this->money->substract($spent, $balance); + $spendGoal = $this->money->substract($balanceSpent, $spendGoal); + $spentTotal = $this->money->add($balanceSpent, $spentTotal); + $balance = $this->money->substract($balanceSpent, $balance); $incoming->setBalance($balance); - $outgoing->setBalance($spent); + $outgoing->setBalance($spentTotal); $outgoing->addFinancedBy($incoming); $this->entityManager->persist($incoming); $this->entityManager->persist($outgoing); $this->entityManager->flush(); - - return $outgoing; } + + return $outgoing; } } From 2d4c04ccf89be8ec1f962fe26cc5056d4dda92b3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 14:13:47 +0100 Subject: [PATCH 56/86] Add checkout processing to wallet --- src/Library/Economy/Payment/WalletGateway.php | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/src/Library/Economy/Payment/WalletGateway.php b/src/Library/Economy/Payment/WalletGateway.php index f8380bb..58bbb1a 100644 --- a/src/Library/Economy/Payment/WalletGateway.php +++ b/src/Library/Economy/Payment/WalletGateway.php @@ -2,10 +2,82 @@ namespace App\Library\Economy\Payment; -abstract class WalletGateway implements GatewayInterface +use App\Entity\AccountingTransaction; +use App\Entity\GatewayChargeType; +use App\Entity\GatewayCheckout; +use App\Entity\Money; +use App\Entity\WalletStatement; +use App\Entity\WalletStatementDirection; +use App\Library\Economy\MoneyService; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class WalletGateway implements GatewayInterface { public static function getName(): string { return 'wallet'; } + + public static function getSupportedChargeTypes(): array + { + return [ + GatewayChargeType::Single, + ]; + } + + public function __construct( + private WalletGatewayService $wallet, + private MoneyService $money, + ) {} + + public function process(GatewayCheckout $checkout): GatewayCheckout + { + $origin = $checkout->getOrigin(); + $available = $this->wallet->getBalance($origin); + $chargeTotal = $this->getChargeTotal($checkout); + + if ($this->money->isLess($available, $chargeTotal)) { + throw new \Exception("Can't spend more than what you have!"); + } + + $charges = $checkout->getCharges(); + foreach ($charges as $charge) { + $transaction = new AccountingTransaction(); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($origin); + $transaction->setTarget($charge->getTarget()); + + $outgoing = new WalletStatement(); + $outgoing->setTransaction($transaction); + $outgoing->setDirection(WalletStatementDirection::Outgoing); + + $outgoing = $this->wallet->spend($transaction); + } + + return $checkout; + } + + public function handleRedirect(Request $request): RedirectResponse + { + return new RedirectResponse(''); + } + + public function handleWebhook(Request $request): Response + { + return new Response(); + } + + private function getChargeTotal(GatewayCheckout $checkout): Money + { + $total = new Money(0, $checkout->getOrigin()->getCurrency()); + + $charges = $checkout->getCharges(); + foreach ($charges as $charge) { + $total = $this->money->add($charge->getMoney(), $total); + } + + return $total; + } } From f6f1bcd042acdf915b9dc017ec17094913b409a5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 14:13:59 +0100 Subject: [PATCH 57/86] Update checkouts pump --- src/Library/Benzina/Pump/CheckoutsPump.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 8d412b4..d56e9c6 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -2,7 +2,6 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; use App\Entity\GatewayCharge; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; @@ -214,11 +213,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $tipjar = new Tipjar(); $tipjar->setName(self::PLATFORM_TIPJAR_NAME); - $accounting = new Accounting(); - $accounting->setTipjar($tipjar); - $this->entityManager->persist($tipjar); - $this->entityManager->persist($accounting); $this->entityManager->flush(); return $tipjar; From ab9b73747c44eaeece9ff62ce9702792657e1836 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 29 Oct 2024 14:14:15 +0100 Subject: [PATCH 58/86] Remove whitespace --- src/Library/Economy/Payment/WalletGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Library/Economy/Payment/WalletGateway.php b/src/Library/Economy/Payment/WalletGateway.php index 58bbb1a..4167a77 100644 --- a/src/Library/Economy/Payment/WalletGateway.php +++ b/src/Library/Economy/Payment/WalletGateway.php @@ -72,7 +72,7 @@ class WalletGateway implements GatewayInterface private function getChargeTotal(GatewayCheckout $checkout): Money { $total = new Money(0, $checkout->getOrigin()->getCurrency()); - + $charges = $checkout->getCharges(); foreach ($charges as $charge) { $total = $this->money->add($charge->getMoney(), $total); From 3f012c9542ee2452c935cfb86f0cc23c3eeea201 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 5 Nov 2024 14:32:59 +0100 Subject: [PATCH 59/86] Move Accounting and Gateway from prefixes to namespaces --- .../Accounting.php} | 12 ++-- .../Gateway.php} | 6 +- src/Entity/{ => Accounting}/Accounting.php | 4 +- .../Transaction.php} | 9 +-- .../{GatewayCharge.php => Gateway/Charge.php} | 33 +++++----- .../ChargeType.php} | 4 +- .../Checkout.php} | 49 ++++++++------- .../CheckoutStatus.php} | 4 +- .../{GatewayLink.php => Gateway/Link.php} | 6 +- .../LinkType.php} | 4 +- .../Tracking.php} | 4 +- .../Interface/AccountingOwnerInterface.php | 2 +- src/Entity/Project.php | 1 + src/Entity/Tipjar.php | 1 + src/Entity/User.php | 1 + src/Entity/WalletStatement.php | 7 ++- src/EventListener/AccountingOwnerListener.php | 2 +- .../WalletTransactionsListener.php | 6 +- src/Library/Benzina/Pump/CheckoutsPump.php | 48 +++++++------- src/Library/Benzina/Pump/ProjectsPump.php | 2 +- .../Economy/Payment/GatewayInterface.php | 20 +++--- src/Library/Economy/Payment/PaypalGateway.php | 52 ++++++++-------- src/Library/Economy/Payment/StripeGateway.php | 62 ++++++++++++------- src/Library/Economy/Payment/WalletGateway.php | 14 ++--- .../Economy/Payment/WalletGatewayService.php | 12 ++-- .../{ => Accounting}/AccountingRepository.php | 4 +- .../TransactionRepository.php} | 22 +++---- .../ChargeRepository.php} | 22 +++---- .../CheckoutRepository.php} | 24 +++---- src/Repository/ProjectRepository.php | 2 +- src/Repository/UserRepository.php | 2 +- src/Repository/WalletStatementRepository.php | 2 +- src/Security/Voter/AccountingVoter.php | 2 +- .../CheckoutService.php} | 32 +++++----- .../AccountingStateProcessor.php | 4 +- .../AccountingStateProvider.php | 4 +- .../SupportedChargeTypesValidator.php | 2 +- .../{ => Accounting}/AccountingTest.php | 4 +- .../Payment/WalletGatewayServiceTest.php | 18 +++--- 39 files changed, 266 insertions(+), 243 deletions(-) rename src/ApiResource/{AccountingApiResource.php => Accounting/Accounting.php} (85%) rename src/ApiResource/{GatewayApiResource.php => Gateway/Gateway.php} (92%) rename src/Entity/{ => Accounting}/Accounting.php (96%) rename src/Entity/{AccountingTransaction.php => Accounting/Transaction.php} (91%) rename src/Entity/{GatewayCharge.php => Gateway/Charge.php} (79%) rename src/Entity/{GatewayChargeType.php => Gateway/ChargeType.php} (78%) rename src/Entity/{GatewayCheckout.php => Gateway/Checkout.php} (82%) rename src/Entity/{GatewayCheckoutStatus.php => Gateway/CheckoutStatus.php} (55%) rename src/Entity/{GatewayLink.php => Gateway/Link.php} (90%) rename src/Entity/{GatewayLinkType.php => Gateway/LinkType.php} (86%) rename src/Entity/{GatewayTracking.php => Gateway/Tracking.php} (87%) rename src/Repository/{ => Accounting}/AccountingRepository.php (95%) rename src/Repository/{AccountingTransactionRepository.php => Accounting/TransactionRepository.php} (53%) rename src/Repository/{GatewayChargeRepository.php => Gateway/ChargeRepository.php} (56%) rename src/Repository/{GatewayCheckoutRepository.php => Gateway/CheckoutRepository.php} (65%) rename src/Service/{GatewayCheckoutService.php => Gateway/CheckoutService.php} (70%) rename src/State/{ => Accounting}/AccountingStateProcessor.php (93%) rename src/State/{ => Accounting}/AccountingStateProvider.php (96%) rename tests/Entity/{ => Accounting}/AccountingTest.php (95%) diff --git a/src/ApiResource/AccountingApiResource.php b/src/ApiResource/Accounting/Accounting.php similarity index 85% rename from src/ApiResource/AccountingApiResource.php rename to src/ApiResource/Accounting/Accounting.php index 44d4f90..127decb 100644 --- a/src/ApiResource/AccountingApiResource.php +++ b/src/ApiResource/Accounting/Accounting.php @@ -1,14 +1,14 @@ + * @var Collection */ #[API\ApiProperty(readableLink: false, writable: false)] - #[ORM\ManyToMany(targetEntity: AccountingTransaction::class, cascade: ['persist'])] + #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; public function __construct() @@ -78,24 +81,24 @@ class GatewayCharge return $this->id; } - public function getCheckout(): ?GatewayCheckout + public function getCheckout(): ?Checkout { return $this->checkout; } - public function setCheckout(?GatewayCheckout $checkout): static + public function setCheckout(?Checkout $checkout): static { $this->checkout = $checkout; return $this; } - public function getType(): ?GatewayChargeType + public function getType(): ?ChargeType { return $this->type; } - public function setType(GatewayChargeType $type): static + public function setType(ChargeType $type): static { $this->type = $type; @@ -151,14 +154,14 @@ class GatewayCharge } /** - * @return Collection + * @return Collection */ public function getTransactions(): Collection { return $this->transactions; } - public function addTransaction(AccountingTransaction $transaction): static + public function addTransaction(Transaction $transaction): static { if (!$this->transactions->contains($transaction)) { $this->transactions->add($transaction); @@ -167,7 +170,7 @@ class GatewayCharge return $this; } - public function removeTransaction(AccountingTransaction $transaction): static + public function removeTransaction(Transaction $transaction): static { $this->transactions->removeElement($transaction); diff --git a/src/Entity/GatewayChargeType.php b/src/Entity/Gateway/ChargeType.php similarity index 78% rename from src/Entity/GatewayChargeType.php rename to src/Entity/Gateway/ChargeType.php index 84c7ba1..da30858 100644 --- a/src/Entity/GatewayChargeType.php +++ b/src/Entity/Gateway/ChargeType.php @@ -1,8 +1,8 @@ 'exact', 'charges.target' => 'exact'])] -#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)] +#[ORM\Entity(repositoryClass: CheckoutRepository::class)] #[ORM\Index(fields: ['migratedId'])] -class GatewayCheckout +class Checkout { use TimestampableCreationEntity; use TimestampableUpdationEntity; @@ -54,18 +55,18 @@ class GatewayCheckout #[API\ApiProperty(writable: false)] #[API\ApiFilter(SearchFilter::class)] #[ORM\Column()] - private ?GatewayCheckoutStatus $status = null; + private ?CheckoutStatus $status = null; /** * The GatewayCharges to be charged at checkout with the gateway. * - * @var Collection + * @var Collection */ #[API\ApiProperty(readableLink: true, writableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] #[SupportedChargeTypes()] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCharge::class, cascade: ['persist'])] + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: Charge::class, cascade: ['persist'])] private Collection $charges; /** @@ -81,7 +82,7 @@ class GatewayCheckout * A list of URLs provided by the Gateway for this checkout.\ * e.g: Fulfill payment, API resource address. * - * @var GatewayLink[] + * @var Link[] */ #[API\ApiProperty(writable: false)] #[ORM\Column] @@ -91,7 +92,7 @@ class GatewayCheckout * A list of tracking codes provided by the Gateway for this checkout.\ * e.g: Order ID, Payment Capture ID, Checkout Session Token. * - * @var GatewayTracking[] + * @var Tracking[] */ #[API\ApiProperty(writable: false)] #[ORM\Column] @@ -119,7 +120,7 @@ class GatewayCheckout public function __construct() { - $this->status = GatewayCheckoutStatus::Pending; + $this->status = CheckoutStatus::Pending; $this->charges = new ArrayCollection(); } @@ -140,12 +141,12 @@ class GatewayCheckout return $this; } - public function getStatus(): ?GatewayCheckoutStatus + public function getStatus(): ?CheckoutStatus { return $this->status; } - public function setStatus(GatewayCheckoutStatus $status): static + public function setStatus(CheckoutStatus $status): static { $this->status = $status; @@ -153,14 +154,14 @@ class GatewayCheckout } /** - * @return Collection + * @return Collection */ public function getCharges(): Collection { return $this->charges; } - public function addCharge(GatewayCharge $charge): static + public function addCharge(Charge $charge): static { if (!$this->charges->contains($charge)) { $this->charges->add($charge); @@ -170,7 +171,7 @@ class GatewayCheckout return $this; } - public function removeCharge(GatewayCharge $charge): static + public function removeCharge(Charge $charge): static { if ($this->charges->removeElement($charge)) { // set the owning side to null (unless already changed) @@ -195,25 +196,25 @@ class GatewayCheckout } /** - * @return GatewayLink[] + * @return Link[] */ public function getGatewayLinks(): array { return $this->gatewayLinks; } - public function addGatewayLink(GatewayLink $link): static + public function addGatewayLink(Link $link): static { $this->gatewayLinks = [...$this->gatewayLinks, $link]; return $this; } - public function removeGatewayLink(GatewayLink $link): static + public function removeGatewayLink(Link $link): static { $this->gatewayLinks = \array_filter( $this->gatewayLinks, - function (GatewayLink $existingLink) use ($link) { + function (Link $existingLink) use ($link) { return $existingLink->href !== $link->href; } ); @@ -222,25 +223,25 @@ class GatewayCheckout } /** - * @return GatewayTracking[] + * @return Tracking[] */ public function getGatewayTrackings(): array { return $this->gatewayTrackings; } - public function addGatewayTracking(GatewayTracking $tracking): static + public function addGatewayTracking(Tracking $tracking): static { $this->gatewayTrackings = [...$this->gatewayTrackings, $tracking]; return $this; } - public function removeGatewayTracking(GatewayTracking $tracking): static + public function removeGatewayTracking(Tracking $tracking): static { $this->gatewayTrackings = \array_filter( $this->gatewayTrackings, - function (GatewayTracking $existingTracking) use ($tracking) { + function (Tracking $existingTracking) use ($tracking) { return $existingTracking->title !== $tracking->title && $existingTracking->value !== $tracking->value; } diff --git a/src/Entity/GatewayCheckoutStatus.php b/src/Entity/Gateway/CheckoutStatus.php similarity index 55% rename from src/Entity/GatewayCheckoutStatus.php rename to src/Entity/Gateway/CheckoutStatus.php index 4e8bc86..a05394e 100644 --- a/src/Entity/GatewayCheckoutStatus.php +++ b/src/Entity/Gateway/CheckoutStatus.php @@ -1,8 +1,8 @@ id; } - public function getTransaction(): ?AccountingTransaction + public function getTransaction(): ?Transaction { return $this->transaction; } - public function setTransaction(AccountingTransaction $transaction): static + public function setTransaction(Transaction $transaction): static { $this->transaction = $transaction; diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php index 8800aa5..eeae8a5 100644 --- a/src/EventListener/AccountingOwnerListener.php +++ b/src/EventListener/AccountingOwnerListener.php @@ -2,7 +2,7 @@ namespace App\EventListener; -use App\Entity\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\ORM\EntityManagerInterface; diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 7d7402b..3d9c4ef 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -2,7 +2,7 @@ namespace App\EventListener; -use App\Entity\AccountingTransaction; +use App\Entity\Accounting\Transaction; use App\Entity\User; use App\Library\Economy\Payment\WalletGatewayService; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; @@ -13,7 +13,7 @@ use Doctrine\ORM\Events; #[AsEntityListener( event: Events::postPersist, method: 'processTransaction', - entity: AccountingTransaction::class + entity: Transaction::class )] final class WalletTransactionsListener { @@ -26,7 +26,7 @@ final class WalletTransactionsListener * Generates a WalletStatement for User-received Transactions. */ public function processTransaction( - AccountingTransaction $transaction, + Transaction $transaction, PostPersistEventArgs $event, ) { $target = $transaction->getTarget(); diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index d56e9c6..5994973 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -2,11 +2,11 @@ namespace App\Library\Benzina\Pump; -use App\Entity\GatewayCharge; -use App\Entity\GatewayChargeType; -use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutStatus; -use App\Entity\GatewayTracking; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\ChargeType; +use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\CheckoutStatus; +use App\Entity\Gateway\Tracking; use App\Entity\Money; use App\Entity\Tipjar; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; @@ -20,7 +20,7 @@ use App\Library\Economy\Payment\WalletGateway; use App\Repository\ProjectRepository; use App\Repository\TipjarRepository; use App\Repository\UserRepository; -use App\Service\GatewayCheckoutService; +use App\Service\Gateway\CheckoutService; use Doctrine\ORM\EntityManagerInterface; class CheckoutsPump extends AbstractPump implements PumpInterface @@ -78,7 +78,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, private EntityManagerInterface $entityManager, - private GatewayCheckoutService $checkoutService, + private CheckoutService $checkoutService, ) {} public function supports(mixed $batch): bool @@ -92,7 +92,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface public function pump(mixed $batch): void { - $batch = $this->skipPumped($batch, 'id', GatewayCheckout::class, 'migratedId'); + $batch = $this->skipPumped($batch, 'id', Checkout::class, 'migratedId'); $tipjar = $this->getPlatformTipjar(); @@ -114,7 +114,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $user = $users[$record['user']]; - $checkout = new GatewayCheckout(); + $checkout = new Checkout(); $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); $checkout->setGateway($this->getCheckoutGateway($record)); @@ -129,7 +129,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->setDateCreated(new \DateTime($record['invested'])); $checkout->setDateUpdated(new \DateTime()); - $charge = new GatewayCharge(); + $charge = new Charge(); $charge->setType($this->getChargeType($record)); $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); @@ -148,8 +148,8 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->addCharge($charge); if ($record['donate_amount'] > 0) { - $tip = new GatewayCharge(); - $tip->setType(GatewayChargeType::Single); + $tip = new Charge(); + $tip->setType(ChargeType::Single); $tip->setTitle(self::CHARGE_TITLE_TIP); $tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency'])); $tip->setTarget($tipjar->getAccounting()); @@ -157,7 +157,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->addCharge($tip); } - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + if ($checkout->getStatus() === CheckoutStatus::Charged) { $checkout = $this->checkoutService->chargeCheckout($checkout); } @@ -219,13 +219,13 @@ class CheckoutsPump extends AbstractPump implements PumpInterface return $tipjar; } - private function getChargeType(array $record): GatewayChargeType + private function getChargeType(array $record): ChargeType { if (\in_array($record['method'], ['stripe_subscription'])) { - return GatewayChargeType::Recurring; + return ChargeType::Recurring; } - return GatewayChargeType::Single; + return ChargeType::Single; } private function getChargeMoney(int $amount, string $currency): Money @@ -239,13 +239,13 @@ class CheckoutsPump extends AbstractPump implements PumpInterface return new Money($amount, $currency); } - private function getCheckoutStatus(array $record): GatewayCheckoutStatus + private function getCheckoutStatus(array $record): CheckoutStatus { if ($record['status'] < 1) { - return GatewayCheckoutStatus::Pending; + return CheckoutStatus::Pending; } - return GatewayCheckoutStatus::Charged; + return CheckoutStatus::Charged; } private function getCheckoutGateway(array $record): string @@ -269,18 +269,18 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } /** - * @return GatewayTracking[] + * @return Tracking[] */ private function getCheckoutTrackings(array $record): array { - $v3Tracking = new GatewayTracking(); + $v3Tracking = new Tracking(); $v3Tracking->title = self::TRACKING_TITLE_V3; $v3Tracking->value = $record['id']; $trackings = [$v3Tracking]; if (!empty($record['payment'])) { - $payment = new GatewayTracking(); + $payment = new Tracking(); $payment->title = self::TRACKING_TITLE_PAYMENT; $payment->value = $record['payment']; @@ -288,7 +288,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } if (!empty($record['transaction'])) { - $transaction = new GatewayTracking(); + $transaction = new Tracking(); $transaction->title = self::TRACKING_TITLE_TRANSACTION; $transaction->value = $record['transaction']; @@ -296,7 +296,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } if (!empty($record['preapproval'])) { - $preapproval = new GatewayTracking(); + $preapproval = new Tracking(); $preapproval->title = self::TRACKING_TITLE_PREAPPROVAL; $preapproval->value = $record['preapproval']; diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index b2ccf1c..dd0edb9 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -2,7 +2,7 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\Project; use App\Entity\ProjectStatus; use App\Entity\User; diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index 0f41ffc..29c028a 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -2,19 +2,19 @@ namespace App\Library\Economy\Payment; -use App\Entity\GatewayCheckout; +use App\Entity\Gateway\Checkout; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** - * Gateway interfaces are in charge of connecting with payment gateways to send them the data in `GatewayCheckout` instances.\ + * Gateway interfaces are in charge of connecting with payment gateways to send them the data in `Checkout` instances.\ * \ * The flow is: - * 1. A client app creates a `GatewayCheckout` record - * 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `GatewayCheckout` - * 3. The interface connects with the gateway and sends the data for the `GatewayCheckout` - * 4. The interface updates the `GatewayCheckout` with the checkout url and reference given by the gateway for the checkout session + * 1. A client app creates a `Checkout` record + * 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `Checkout` + * 3. The interface connects with the gateway and sends the data for the `Checkout` + * 4. The interface updates the `Checkout` with the checkout url and reference given by the gateway for the checkout session * 5. `App\State\GatewayCheckoutProcessor` returns the response data to the client app * 6. The client app redirects the user to the payment with the gateway * 7. The gateway processes the payment and redirects the user back to v4 @@ -29,18 +29,18 @@ interface GatewayInterface public static function getName(): string; /** - * @return \App\Entity\GatewayChargeType[] The charge types that can be processed by this Gateway + * @return \App\Entity\Gateway\ChargeType[] The charge types that can be processed by this Gateway */ public static function getSupportedChargeTypes(): array; /** * Connects with the payment gateway and creates a checkout session so the gateway can process the payment. * - * @param GatewayCheckout $checkout The GatewayCheckout with the data for the payment to be charged + * @param Checkout $checkout The Checkout with the data for the payment to be charged * - * @return GatewayCheckout The GatewayCheckout updated with the data given by the gateway + * @return Checkout The Checkout updated with the data given by the gateway */ - public function process(GatewayCheckout $checkout): GatewayCheckout; + public function process(Checkout $checkout): Checkout; /** * When a user is redirected by the gateway we must handle the redirection diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 1f9f542..7269f1e 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -3,15 +3,15 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Entity\GatewayCharge; -use App\Entity\GatewayChargeType; -use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutStatus; -use App\Entity\GatewayLink; -use App\Entity\GatewayLinkType; -use App\Entity\GatewayTracking; -use App\Repository\GatewayCheckoutRepository; -use App\Service\GatewayCheckoutService; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\ChargeType; +use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\CheckoutStatus; +use App\Entity\Gateway\Link; +use App\Entity\Gateway\LinkType; +use App\Entity\Gateway\Tracking; +use App\Repository\Gateway\CheckoutRepository; +use App\Service\Gateway\CheckoutService; use Brick\Money\Money as BrickMoney; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -48,8 +48,8 @@ class PaypalGateway implements GatewayInterface private RouterInterface $router, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, - private GatewayCheckoutService $checkoutService, - private GatewayCheckoutRepository $checkoutRepository, + private CheckoutService $checkoutService, + private CheckoutRepository $checkoutRepository, ) {} public static function getName(): string @@ -60,11 +60,11 @@ class PaypalGateway implements GatewayInterface public static function getSupportedChargeTypes(): array { return [ - GatewayChargeType::Single, + ChargeType::Single, ]; } - public function process(GatewayCheckout $checkout): GatewayCheckout + public function process(Checkout $checkout): Checkout { $order = $this->paypal->postOrder([ 'intent' => self::PAYPAL_ORDER_INTENT, @@ -72,7 +72,7 @@ class PaypalGateway implements GatewayInterface 'payment_source' => $this->getPaypalPaymentSource($checkout), ]); - $tracking = new GatewayTracking(); + $tracking = new Tracking(); $tracking->title = self::TRACKING_TITLE_ORDER; $tracking->value = $order['id']; @@ -80,10 +80,10 @@ class PaypalGateway implements GatewayInterface foreach ($order['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) - ? GatewayLinkType::Payment - : GatewayLinkType::Debug; + ? LinkType::Payment + : LinkType::Debug; - $link = new GatewayLink(); + $link = new Link(); $link->href = $linkData['href']; $link->rel = $linkData['rel']; $link->method = $linkData['method']; @@ -97,7 +97,7 @@ class PaypalGateway implements GatewayInterface public function handleRedirect(Request $request): RedirectResponse { - if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { + if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) { throw new \Exception(sprintf('Checkout was not completed successfully.')); } @@ -105,10 +105,10 @@ class PaypalGateway implements GatewayInterface $checkout = $this->checkoutRepository->find($checkoutId); if ($checkout === null) { - throw new \Exception(sprintf("GatewayCheckout '%s' could not be found.", $checkoutId)); + throw new \Exception(sprintf("Checkout '%s' could not be found.", $checkoutId)); } - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + if ($checkout->getStatus() === CheckoutStatus::Charged) { return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } @@ -128,7 +128,7 @@ class PaypalGateway implements GatewayInterface } foreach ($capture['purchase_units'] as $purchaseUnit) { - $tracking = new GatewayTracking(); + $tracking = new Tracking(); $tracking->title = self::TRACKING_TITLE_TRANSACTION; $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; @@ -174,11 +174,11 @@ class PaypalGateway implements GatewayInterface $checkout = $this->checkoutRepository->findOneByTracking(self::TRACKING_TITLE_ORDER, $orderId); if ($checkout === null) { - throw new \Exception(sprintf("Could not find any GatewayCheckout by the GatewayTracking '%s'", $orderId), 1); + throw new \Exception(sprintf("Could not find any Checkout by the Tracking '%s'", $orderId), 1); } foreach ($event['resource']['purchase_units'] as $purchaseUnit) { - $tracking = new GatewayTracking(); + $tracking = new Tracking(); $tracking->title = self::TRACKING_TITLE_TRANSACTION; $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; @@ -188,7 +188,7 @@ class PaypalGateway implements GatewayInterface $checkout = $this->checkoutService->chargeCheckout($checkout); } - private function getPaypalMoney(GatewayCharge $charge): array + private function getPaypalMoney(Charge $charge): array { $brick = BrickMoney::ofMinor( $charge->getMoney()->amount, @@ -201,7 +201,7 @@ class PaypalGateway implements GatewayInterface ]; } - private function getPaypalPurchaseUnits(GatewayCheckout $checkout): array + private function getPaypalPurchaseUnits(Checkout $checkout): array { $units = []; @@ -236,7 +236,7 @@ class PaypalGateway implements GatewayInterface return $units; } - private function getPaypalPaymentSource(GatewayCheckout $checkout): array + private function getPaypalPaymentSource(Checkout $checkout): array { return [ 'paypal' => [ diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 5060537..4a79510 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -3,14 +3,16 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Entity\GatewayChargeType; -use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutStatus; -use App\Entity\GatewayLink; -use App\Entity\GatewayLinkType; -use App\Entity\GatewayTracking; -use App\Repository\GatewayCheckoutRepository; -use App\Service\GatewayCheckoutService; +use App\Entity\Gateway\ChargeType; +use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\CheckoutStatus; +use App\Entity\Gateway\Link; +use App\Entity\Gateway\LinkType; +use App\Entity\Gateway\Tracking; +use App\Entity\User; +use App\Repository\Gateway\CheckoutRepository; +use App\Repository\UserRepository; +use App\Service\Gateway\CheckoutService; use Doctrine\ORM\EntityManagerInterface; use Stripe\Checkout\Session as StripeSession; use Stripe\StripeClient; @@ -31,8 +33,9 @@ class StripeGateway implements GatewayInterface private string $stripeApiKey, private string $stripeWebhookSecret, private RouterInterface $router, - private GatewayCheckoutService $checkoutService, - private GatewayCheckoutRepository $checkoutRepository, + private CheckoutService $checkoutService, + private CheckoutRepository $checkoutRepository, + private UserRepository $userRepository, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, ) { @@ -47,15 +50,15 @@ class StripeGateway implements GatewayInterface public static function getSupportedChargeTypes(): array { return [ - GatewayChargeType::Single, - GatewayChargeType::Recurring, + ChargeType::Single, + ChargeType::Recurring, ]; } - public function process(GatewayCheckout $checkout): GatewayCheckout + public function process(Checkout $checkout): Checkout { $session = $this->stripe->checkout->sessions->create([ - 'customer_email' => $checkout->getOrigin()->getUser()->getEmail(), + 'customer_email' => $this->getStripeCustomer($checkout), 'mode' => $this->getStripeMode($checkout), 'line_items' => $this->getStripeLineItems($checkout), // Because Symfony's Router encodes query parameters, the value {CHECKOUT_SESSION_ID} @@ -65,16 +68,16 @@ class StripeGateway implements GatewayInterface 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)), ]); - $link = new GatewayLink(); + $link = new Link(); $link->href = $session->url; $link->rel = 'approve'; $link->method = Request::METHOD_GET; - $link->type = GatewayLinkType::Payment; + $link->type = LinkType::Payment; $checkout->addGatewayLink($link); - $tracking = new GatewayTracking(); + $tracking = new Tracking(); $tracking->title = self::TRACKING_TITLE_CHECKOUT; $tracking->value = $session->id; @@ -91,14 +94,14 @@ class StripeGateway implements GatewayInterface $checkout = $this->checkoutRepository->find($request->query->get('checkoutId')); if ($checkout === null) { - throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId)); + throw new \Exception(sprintf("Stripe checkout '%s' exists but no Checkout with that reference was found.", $sessionId)); } - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + if ($checkout->getStatus() === CheckoutStatus::Charged) { return $checkout; } - if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { + if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) { return $checkout; } @@ -132,10 +135,21 @@ class StripeGateway implements GatewayInterface } } - private function getStripeMode(GatewayCheckout $checkout): string + private function getStripeCustomer(Checkout $checkout): string + { + if ($checkout->getOrigin()->getOwnerClass() !== User::class) { + return ''; + } + + $user = $this->userRepository->find($checkout->getOrigin()->getOwnerId()); + + return $user->getEmail(); + } + + private function getStripeMode(Checkout $checkout): string { foreach ($checkout->getCharges() as $charge) { - if ($charge->getType() === GatewayChargeType::Recurring) { + if ($charge->getType() === ChargeType::Recurring) { return StripeSession::MODE_SUBSCRIPTION; } } @@ -143,7 +157,7 @@ class StripeGateway implements GatewayInterface return StripeSession::MODE_PAYMENT; } - private function getStripeLineItems(GatewayCheckout $checkout): array + private function getStripeLineItems(Checkout $checkout): array { $items = []; @@ -157,7 +171,7 @@ class StripeGateway implements GatewayInterface ], ]; - if ($charge->getType() === GatewayChargeType::Recurring) { + if ($charge->getType() === ChargeType::Recurring) { $price['recurring'] = ['interval' => 'month']; } diff --git a/src/Library/Economy/Payment/WalletGateway.php b/src/Library/Economy/Payment/WalletGateway.php index 4167a77..7dc363f 100644 --- a/src/Library/Economy/Payment/WalletGateway.php +++ b/src/Library/Economy/Payment/WalletGateway.php @@ -2,9 +2,9 @@ namespace App\Library\Economy\Payment; -use App\Entity\AccountingTransaction; -use App\Entity\GatewayChargeType; -use App\Entity\GatewayCheckout; +use App\Entity\Accounting\Transaction; +use App\Entity\Gateway\ChargeType; +use App\Entity\Gateway\Checkout; use App\Entity\Money; use App\Entity\WalletStatement; use App\Entity\WalletStatementDirection; @@ -23,7 +23,7 @@ class WalletGateway implements GatewayInterface public static function getSupportedChargeTypes(): array { return [ - GatewayChargeType::Single, + ChargeType::Single, ]; } @@ -32,7 +32,7 @@ class WalletGateway implements GatewayInterface private MoneyService $money, ) {} - public function process(GatewayCheckout $checkout): GatewayCheckout + public function process(Checkout $checkout): Checkout { $origin = $checkout->getOrigin(); $available = $this->wallet->getBalance($origin); @@ -44,7 +44,7 @@ class WalletGateway implements GatewayInterface $charges = $checkout->getCharges(); foreach ($charges as $charge) { - $transaction = new AccountingTransaction(); + $transaction = new Transaction(); $transaction->setMoney($charge->getMoney()); $transaction->setOrigin($origin); $transaction->setTarget($charge->getTarget()); @@ -69,7 +69,7 @@ class WalletGateway implements GatewayInterface return new Response(); } - private function getChargeTotal(GatewayCheckout $checkout): Money + private function getChargeTotal(Checkout $checkout): Money { $total = new Money(0, $checkout->getOrigin()->getCurrency()); diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Library/Economy/Payment/WalletGatewayService.php index 4cec990..ffc7ddd 100644 --- a/src/Library/Economy/Payment/WalletGatewayService.php +++ b/src/Library/Economy/Payment/WalletGatewayService.php @@ -2,8 +2,8 @@ namespace App\Library\Economy\Payment; -use App\Entity\Accounting; -use App\Entity\AccountingTransaction; +use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; use App\Entity\Money; use App\Entity\WalletStatement; use App\Entity\WalletStatementDirection; @@ -52,11 +52,11 @@ class WalletGatewayService /** * Puts the money of a Transaction into the target wallet. * - * @param AccountingTransaction $transaction The Transaction targetting a wallet + * @param Transaction $transaction The Transaction targetting a wallet * * @return WalletStatement An incoming statement for the target wallet */ - public function save(AccountingTransaction $transaction): WalletStatement + public function save(Transaction $transaction): WalletStatement { $statement = new WalletStatement(); $statement->setTransaction($transaction); @@ -72,11 +72,11 @@ class WalletGatewayService /** * Takes the money of a Transaction from the origin wallet. * - * @param AccountingTransaction $transaction The Transaction originating from a wallet + * @param Transaction $transaction The Transaction originating from a wallet * * @return WalletStatement An outgoing statement financed by previous incoming statements */ - public function spend(AccountingTransaction $transaction): WalletStatement + public function spend(Transaction $transaction): WalletStatement { $origin = $transaction->getOrigin(); diff --git a/src/Repository/AccountingRepository.php b/src/Repository/Accounting/AccountingRepository.php similarity index 95% rename from src/Repository/AccountingRepository.php rename to src/Repository/Accounting/AccountingRepository.php index c32c81e..d871046 100644 --- a/src/Repository/AccountingRepository.php +++ b/src/Repository/Accounting/AccountingRepository.php @@ -1,8 +1,8 @@ + * @extends ServiceEntityRepository * - * @method AccountingTransaction|null find($id, $lockMode = null, $lockVersion = null) - * @method AccountingTransaction|null findOneBy(array $criteria, array $orderBy = null) - * @method AccountingTransaction[] findAll() - * @method AccountingTransaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @method Transaction|null find($id, $lockMode = null, $lockVersion = null) + * @method Transaction|null findOneBy(array $criteria, array $orderBy = null) + * @method Transaction[] findAll() + * @method Transaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class AccountingTransactionRepository extends ServiceEntityRepository +class TransactionRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, AccountingTransaction::class); + parent::__construct($registry, Transaction::class); } // /** - // * @return AccountingTransaction[] Returns an array of AccountingTransaction objects + // * @return Transaction[] Returns an array of Transaction objects // */ // public function findByExampleField($value): array // { @@ -36,7 +36,7 @@ class AccountingTransactionRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?AccountingTransaction + // public function findOneBySomeField($value): ?Transaction // { // return $this->createQueryBuilder('t') // ->andWhere('t.exampleField = :val') diff --git a/src/Repository/GatewayChargeRepository.php b/src/Repository/Gateway/ChargeRepository.php similarity index 56% rename from src/Repository/GatewayChargeRepository.php rename to src/Repository/Gateway/ChargeRepository.php index 9748d28..c8a196c 100644 --- a/src/Repository/GatewayChargeRepository.php +++ b/src/Repository/Gateway/ChargeRepository.php @@ -1,28 +1,28 @@ + * @extends ServiceEntityRepository * - * @method GatewayCharge|null find($id, $lockMode = null, $lockVersion = null) - * @method GatewayCharge|null findOneBy(array $criteria, array $orderBy = null) - * @method GatewayCharge[] findAll() - * @method GatewayCharge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @method Charge|null find($id, $lockMode = null, $lockVersion = null) + * @method Charge|null findOneBy(array $criteria, array $orderBy = null) + * @method Charge[] findAll() + * @method Charge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class GatewayChargeRepository extends ServiceEntityRepository +class ChargeRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, GatewayCharge::class); + parent::__construct($registry, Charge::class); } // /** - // * @return GatewayCharge[] Returns an array of GatewayCharge objects + // * @return Charge[] Returns an array of Charge objects // */ // public function findByExampleField($value): array // { @@ -36,7 +36,7 @@ class GatewayChargeRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?GatewayCharge + // public function findOneBySomeField($value): ?Charge // { // return $this->createQueryBuilder('g') // ->andWhere('g.exampleField = :val') diff --git a/src/Repository/GatewayCheckoutRepository.php b/src/Repository/Gateway/CheckoutRepository.php similarity index 65% rename from src/Repository/GatewayCheckoutRepository.php rename to src/Repository/Gateway/CheckoutRepository.php index 9d8ee48..ad73efa 100644 --- a/src/Repository/GatewayCheckoutRepository.php +++ b/src/Repository/Gateway/CheckoutRepository.php @@ -1,29 +1,29 @@ + * @extends ServiceEntityRepository * - * @method GatewayCheckout|null find($id, $lockMode = null, $lockVersion = null) - * @method GatewayCheckout|null findOneBy(array $criteria, array $orderBy = null) - * @method GatewayCheckout[] findAll() - * @method GatewayCheckout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @method Checkout|null find($id, $lockMode = null, $lockVersion = null) + * @method Checkout|null findOneBy(array $criteria, array $orderBy = null) + * @method Checkout[] findAll() + * @method Checkout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class GatewayCheckoutRepository extends ServiceEntityRepository +class CheckoutRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, GatewayCheckout::class); + parent::__construct($registry, Checkout::class); } /** - * @return GatewayCheckout|null + * @return Checkout|null */ public function findOneByTracking(string $title, string $value): array { @@ -39,7 +39,7 @@ class GatewayCheckoutRepository extends ServiceEntityRepository } // /** - // * @return GatewayCheckout[] Returns an array of GatewayCheckout objects + // * @return Checkout[] Returns an array of Checkout objects // */ // public function findByExampleField($value): array // { @@ -53,7 +53,7 @@ class GatewayCheckoutRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?GatewayCheckout + // public function findOneBySomeField($value): ?Checkout // { // return $this->createQueryBuilder('g') // ->andWhere('g.exampleField = :val') diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index a1a9495..ffd8d5e 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -2,7 +2,7 @@ namespace App\Repository; -use App\Entity\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\Project; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index bc4abf3..31bb5e7 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -2,7 +2,7 @@ namespace App\Repository; -use App\Entity\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; diff --git a/src/Repository/WalletStatementRepository.php b/src/Repository/WalletStatementRepository.php index 5277a5e..8987513 100644 --- a/src/Repository/WalletStatementRepository.php +++ b/src/Repository/WalletStatementRepository.php @@ -2,7 +2,7 @@ namespace App\Repository; -use App\Entity\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\WalletStatement; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query\Expr\Join; diff --git a/src/Security/Voter/AccountingVoter.php b/src/Security/Voter/AccountingVoter.php index 77ce9c8..eccd006 100644 --- a/src/Security/Voter/AccountingVoter.php +++ b/src/Security/Voter/AccountingVoter.php @@ -14,7 +14,7 @@ class AccountingVoter extends Voter protected function supports(string $attribute, mixed $subject): bool { return in_array($attribute, [self::EDIT]) - && $subject instanceof \App\Entity\Accounting; + && $subject instanceof \App\Entity\Accounting\Accounting; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/Gateway/CheckoutService.php similarity index 70% rename from src/Service/GatewayCheckoutService.php rename to src/Service/Gateway/CheckoutService.php index 0d5efaa..2ad5ac7 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/Gateway/CheckoutService.php @@ -1,15 +1,15 @@ setStatus(GatewayCheckoutStatus::Charged); + $checkout->setStatus(CheckoutStatus::Charged); foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); + $transaction = new Transaction(); $transaction->setMoney($charge->getMoney()); $transaction->setOrigin($checkout->getOrigin()); $transaction->setTarget($charge->getTarget()); diff --git a/src/State/AccountingStateProcessor.php b/src/State/Accounting/AccountingStateProcessor.php similarity index 93% rename from src/State/AccountingStateProcessor.php rename to src/State/Accounting/AccountingStateProcessor.php index f2e65c7..1645b65 100644 --- a/src/State/AccountingStateProcessor.php +++ b/src/State/Accounting/AccountingStateProcessor.php @@ -1,11 +1,11 @@ */ + /** @var Collection */ $charges = $value; $checkout = $charges->toArray()[0]->getCheckout(); diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/Accounting/AccountingTest.php similarity index 95% rename from tests/Entity/AccountingTest.php rename to tests/Entity/Accounting/AccountingTest.php index 89aa398..4946e70 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/Accounting/AccountingTest.php @@ -1,8 +1,8 @@ assertEquals(0, $balance->amount); $this->assertEquals($user->getCurrency(), $balance->currency); - $incoming = new AccountingTransaction(); + $incoming = new Transaction(); $incoming->setMoney(new Money(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -101,7 +101,7 @@ class WalletGatewayServiceTest extends KernelTestCase $tipjar = $this->getTipjar()->getAccounting(); $user = $this->getUser()->getAccounting(); - $incoming = new AccountingTransaction(); + $incoming = new Transaction(); $incoming->setMoney(new Money(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -109,7 +109,7 @@ class WalletGatewayServiceTest extends KernelTestCase $this->entityManager->persist($incoming); $this->entityManager->flush(); - $outgoing = new AccountingTransaction(); + $outgoing = new Transaction(); $outgoing->setMoney(new Money(20, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -139,7 +139,7 @@ class WalletGatewayServiceTest extends KernelTestCase $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); - $outgoing = new AccountingTransaction(); + $outgoing = new Transaction(); $outgoing->setMoney(new Money(30, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -182,7 +182,7 @@ class WalletGatewayServiceTest extends KernelTestCase $tipjar = $this->getTipjar()->getAccounting(); $user = $this->getUser()->getAccounting(); - $incoming = new AccountingTransaction(); + $incoming = new Transaction(); $incoming->setMoney(new Money(10, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -190,7 +190,7 @@ class WalletGatewayServiceTest extends KernelTestCase $this->entityManager->persist($incoming); $this->entityManager->flush(); - $incoming = new AccountingTransaction(); + $incoming = new Transaction(); $incoming->setMoney(new Money(10, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -198,14 +198,14 @@ class WalletGatewayServiceTest extends KernelTestCase $this->entityManager->persist($incoming); $this->entityManager->flush(); - $outgoing = new AccountingTransaction(); + $outgoing = new Transaction(); $outgoing->setMoney(new Money(15, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); $this->walletService->spend($outgoing); - $incoming = new AccountingTransaction(); + $incoming = new Transaction(); $incoming->setMoney(new Money(5, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); From 0e786f8ab209dba1fc7d2e93f4433466b8403967 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 5 Nov 2024 16:09:08 +0100 Subject: [PATCH 60/86] Move Gateways outside of Library --- .env | 8 ++--- config/services.yaml | 36 +++++++++---------- src/ApiResource/Gateway/Gateway.php | 6 ++-- src/Command/GatewaysSetupCommand.php | 2 +- src/Controller/GatewaysController.php | 2 +- .../Compiler/GatewaysCompilerPass.php | 25 ++++++++++--- src/Entity/Gateway/Charge.php | 1 + src/Entity/Gateway/Checkout.php | 7 ++-- src/Entity/WalletStatement.php | 9 ++--- .../WalletTransactionsListener.php | 4 +-- src/{Entity => }/Gateway/ChargeType.php | 2 +- src/{Entity => }/Gateway/CheckoutStatus.php | 2 +- .../Gateway}/CashGateway.php | 4 ++- .../Gateway}/CecaGateway.php | 4 ++- .../Gateway}/DropGateway.php | 4 ++- .../Payment => Gateway}/GatewayInterface.php | 4 +-- .../Payment => Gateway}/GatewayLocator.php | 6 ++-- src/{Entity => }/Gateway/Link.php | 2 +- src/{Entity => }/Gateway/LinkType.php | 2 +- .../Paypal}/PaypalGateway.php | 17 +++++---- .../Paypal/PaypalService.php} | 4 +-- .../Stripe}/StripeGateway.php | 13 +++---- src/{Entity => }/Gateway/Tracking.php | 2 +- .../Wallet/StatementDirection.php} | 4 +-- .../Wallet}/WalletGateway.php | 10 +++--- .../Wallet/WalletService.php} | 13 ++++--- src/Library/Benzina/Pump/CheckoutsPump.php | 18 +++++----- .../CheckoutStateProcessor.php} | 6 ++-- .../{ => Gateway}/GatewayStateProvider.php | 12 +++---- src/Validator/GatewayName.php | 2 +- src/Validator/GatewayNameValidator.php | 2 +- .../SupportedChargeTypesValidator.php | 2 +- .../Paypal/PaypalServiceTest.php} | 10 +++--- .../Wallet/WalletServiceTest.php} | 32 ++++++++--------- 34 files changed, 151 insertions(+), 126 deletions(-) rename src/{Entity => }/Gateway/ChargeType.php (88%) rename src/{Entity => }/Gateway/CheckoutStatus.php (77%) rename src/{Library/Economy/Payment => Gateway/Gateway}/CashGateway.php (69%) rename src/{Library/Economy/Payment => Gateway/Gateway}/CecaGateway.php (68%) rename src/{Library/Economy/Payment => Gateway/Gateway}/DropGateway.php (69%) rename src/{Library/Economy/Payment => Gateway}/GatewayInterface.php (95%) rename src/{Library/Economy/Payment => Gateway}/GatewayLocator.php (94%) rename src/{Entity => }/Gateway/Link.php (96%) rename src/{Entity => }/Gateway/LinkType.php (92%) rename src/{Library/Economy/Payment => Gateway/Paypal}/PaypalGateway.php (95%) rename src/{Library/Economy/Payment/PaypalGatewayService.php => Gateway/Paypal/PaypalService.php} (98%) rename src/{Library/Economy/Payment => Gateway/Stripe}/StripeGateway.php (96%) rename src/{Entity => }/Gateway/Tracking.php (91%) rename src/{Entity/WalletStatementDirection.php => Gateway/Wallet/StatementDirection.php} (77%) rename src/{Library/Economy/Payment => Gateway/Wallet}/WalletGateway.php (89%) rename src/{Library/Economy/Payment/WalletGatewayService.php => Gateway/Wallet/WalletService.php} (90%) rename src/State/{GatewayCheckoutProcessor.php => Gateway/CheckoutStateProcessor.php} (87%) rename src/State/{ => Gateway}/GatewayStateProvider.php (78%) rename tests/{Library/Economy/Payment/PaypalGatewayServiceTest.php => Gateway/Paypal/PaypalServiceTest.php} (86%) rename tests/{Library/Economy/Payment/WalletGatewayServiceTest.php => Gateway/Wallet/WalletServiceTest.php} (85%) diff --git a/.env b/.env index 5d6a1b5..ff59b19 100644 --- a/.env +++ b/.env @@ -39,15 +39,15 @@ DOCTRINE_ENCRYPT_KEY="" CRYPTO_SECRET="" ###< doctrineencryptbundle/doctrine-encrypt-bundle ### -###> App\Library\Economy\Payment\StripeGateway ### +###> App\Gateway\Stripe\StripeGateway ### STRIPE_API_KEY="" STRIPE_WEBHOOK_SECRET="" -###< App\Library\Economy\Payment\StripeGateway ### +###< App\Gateway\Stripe\StripeGateway ### -###> App\Library\Economy\Payment\PaypalGateway ### +###> App\Gateway\Paypal\PaypalGateway ### # PAYPAL_API_ADDRESS="https://api-m.paypal.com" PAYPAL_API_ADDRESS="https://api-m.sandbox.paypal.com" PAYPAL_CLIENT_ID="" PAYPAL_CLIENT_SECRET="" PAYPAL_WEBHOOK_ID="WEBHOOK_ID" -###< App\Library\Economy\Payment\PaypalGateway ### +###< App\Gateway\Paypal\PaypalGateway ### diff --git a/config/services.yaml b/config/services.yaml index 1e06935..3b5579e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -12,12 +12,12 @@ services: autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. _instanceof: + App\Gateway\GatewayInterface: + tags: ['app.gateway.gateway'] App\Library\Benzina\Pump\PumpInterface: tags: ['app.lib.benzina.pump.pump'] App\Library\Economy\Currency\ExchangeInterface: tags: ['app.lib.economy.currency.exchange'] - App\Library\Economy\Payment\GatewayInterface: - tags: ['app.lib.economy.payment.gateway'] # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name @@ -37,6 +37,22 @@ services: arguments: - '@App\OpenApi\OpenApiFactory.inner' + App\Gateway\GatewayLocator: + arguments: + - !tagged 'app.gateway.gateway' + + App\Gateway\Paypal\PaypalService: + arguments: + - '%env(PAYPAL_API_ADDRESS)%' + - '%env(PAYPAL_CLIENT_ID)%' + - '%env(PAYPAL_CLIENT_SECRET)%' + - '%env(PAYPAL_WEBHOOK_ID)%' + + App\Gateway\Stripe\StripeGateway: + arguments: + - '%env(STRIPE_API_KEY)%' + - '%env(STRIPE_WEBHOOK_SECRET)%' + App\Service\Auth\AuthService: arguments: - '%env(APP_SECRET)%' @@ -49,21 +65,5 @@ services: arguments: - !tagged 'app.lib.economy.currency.exchange' - App\Library\Economy\Payment\GatewayLocator: - arguments: - - !tagged 'app.lib.economy.payment.gateway' - - App\Library\Economy\Payment\StripeGateway: - arguments: - - '%env(STRIPE_API_KEY)%' - - '%env(STRIPE_WEBHOOK_SECRET)%' - - App\Library\Economy\Payment\PaypalGatewayService: - arguments: - - '%env(PAYPAL_API_ADDRESS)%' - - '%env(PAYPAL_CLIENT_ID)%' - - '%env(PAYPAL_CLIENT_SECRET)%' - - '%env(PAYPAL_WEBHOOK_ID)%' - # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/src/ApiResource/Gateway/Gateway.php b/src/ApiResource/Gateway/Gateway.php index 411fd83..795cb14 100644 --- a/src/ApiResource/Gateway/Gateway.php +++ b/src/ApiResource/Gateway/Gateway.php @@ -3,8 +3,8 @@ namespace App\ApiResource\Gateway; use ApiPlatform\Metadata as API; -use App\Library\Economy\Payment\GatewayInterface; -use App\State\GatewayStateProvider; +use App\Gateway\GatewayInterface; +use App\State\Gateway\GatewayStateProvider; /** * Gateways represent payment services used to perform the necessary payments for Transactions between Accounts.\ @@ -29,7 +29,7 @@ class Gateway /** * The GatewayCharge types that can be processed by this Gateway. * - * @return \App\Entity\Gateway\ChargeType[] + * @return \App\Gateway\ChargeType[] */ public function getSupported(): array { diff --git a/src/Command/GatewaysSetupCommand.php b/src/Command/GatewaysSetupCommand.php index b9bf75c..97b3bb9 100644 --- a/src/Command/GatewaysSetupCommand.php +++ b/src/Command/GatewaysSetupCommand.php @@ -3,7 +3,7 @@ namespace App\Command; use App\DependencyInjection\Compiler\GatewaysCompilerPass; -use App\Library\Economy\Payment\GatewayLocator; +use App\Gateway\GatewayLocator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Controller/GatewaysController.php b/src/Controller/GatewaysController.php index 4640201..598a154 100644 --- a/src/Controller/GatewaysController.php +++ b/src/Controller/GatewaysController.php @@ -2,7 +2,7 @@ namespace App\Controller; -use App\Library\Economy\Payment\GatewayLocator; +use App\Gateway\GatewayLocator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php index 0bb40f5..21acaf7 100755 --- a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php +++ b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php @@ -2,7 +2,7 @@ namespace App\DependencyInjection\Compiler; -use App\Library\Economy\Payment\GatewayInterface; +use App\Gateway\GatewayInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -72,9 +72,8 @@ class GatewaysCompilerPass implements CompilerPassInterface return $names; } - private static function getGatewayClasses(string $classesDir): array + private static function getGatewayClasses(string $classesDir, string $namespace): array { - $namespace = self::getGatewaysNamespace(); $economyDirPaths = \scandir($classesDir); $classes = []; @@ -83,6 +82,20 @@ class GatewaysCompilerPass implements CompilerPassInterface continue; } + $fullpath = sprintf('%s%s%s', $classesDir, \DIRECTORY_SEPARATOR, $path); + + if (\is_dir($fullpath)) { + $classes = [ + ...$classes, + ...self::getGatewayClasses( + $fullpath, + sprintf('%s\%s', $namespace, $path) + ), + ]; + + continue; + } + $className = \rtrim($path, '.php'); if (!\str_ends_with($className, 'Gateway')) { continue; @@ -141,13 +154,15 @@ class GatewaysCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { + $namespace = self::getGatewaysNamespace(); + $classesDir = join(DIRECTORY_SEPARATOR, [ $container->getParameter('kernel.project_dir'), 'src', - ...\array_slice(explode('\\', self::getGatewaysNamespace()), 1), + ...\array_slice(explode('\\', $namespace), 1), ]); - $gatewayClasses = self::getGatewayClasses($classesDir); + $gatewayClasses = self::getGatewayClasses($classesDir, $namespace); self::validateGatewayNames($gatewayClasses); $gatewayNames = $this->getGatewayNames($gatewayClasses); diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index b818546..b2821ed 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -6,6 +6,7 @@ use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; use App\Entity\Money; +use App\Gateway\ChargeType; use App\Repository\Gateway\ChargeRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 7f1ef8f..6832ffa 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -7,8 +7,11 @@ use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Trait\TimestampableCreationEntity; use App\Entity\Trait\TimestampableUpdationEntity; +use App\Gateway\CheckoutStatus; +use App\Gateway\Link; +use App\Gateway\Tracking; use App\Repository\Gateway\CheckoutRepository; -use App\State\GatewayCheckoutProcessor; +use App\State\Gateway\CheckoutStateProcessor; use App\Validator\GatewayName; use App\Validator\SupportedChargeTypes; use Doctrine\Common\Collections\ArrayCollection; @@ -25,7 +28,7 @@ use Symfony\Component\Validator\Constraints as Assert; */ #[Gedmo\Loggable()] #[API\GetCollection()] -#[API\Post(processor: GatewayCheckoutProcessor::class)] +#[API\Post(processor: CheckoutStateProcessor::class)] #[API\Get()] #[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])] #[ORM\Entity(repositoryClass: CheckoutRepository::class)] diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index b8a4e0d..4f109f4 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -4,6 +4,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use App\Entity\Accounting\Transaction; +use App\Gateway\Wallet\StatementDirection; use App\Repository\WalletStatementRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -22,8 +23,8 @@ class WalletStatement #[ORM\JoinColumn(nullable: false)] private ?Transaction $transaction = null; - #[ORM\Column(enumType: WalletStatementDirection::class)] - private ?WalletStatementDirection $direction = null; + #[ORM\Column(enumType: StatementDirection::class)] + private ?StatementDirection $direction = null; #[ORM\Embedded(class: Money::class)] private ?Money $balance = null; @@ -68,12 +69,12 @@ class WalletStatement return $this; } - public function getDirection(): ?WalletStatementDirection + public function getDirection(): ?StatementDirection { return $this->direction; } - public function setDirection(WalletStatementDirection $direction): static + public function setDirection(StatementDirection $direction): static { $this->direction = $direction; diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 3d9c4ef..73d188d 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -4,7 +4,7 @@ namespace App\EventListener; use App\Entity\Accounting\Transaction; use App\Entity\User; -use App\Library\Economy\Payment\WalletGatewayService; +use App\Gateway\Wallet\WalletService; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; @@ -19,7 +19,7 @@ final class WalletTransactionsListener { public function __construct( private EntityManagerInterface $entityManager, - private WalletGatewayService $wallet, + private WalletService $wallet, ) {} /** diff --git a/src/Entity/Gateway/ChargeType.php b/src/Gateway/ChargeType.php similarity index 88% rename from src/Entity/Gateway/ChargeType.php rename to src/Gateway/ChargeType.php index da30858..15725d1 100644 --- a/src/Entity/Gateway/ChargeType.php +++ b/src/Gateway/ChargeType.php @@ -1,6 +1,6 @@ getGateway(); if (!$gateway) { diff --git a/src/Entity/Gateway/Link.php b/src/Gateway/Link.php similarity index 96% rename from src/Entity/Gateway/Link.php rename to src/Gateway/Link.php index a2c501a..40cb124 100644 --- a/src/Entity/Gateway/Link.php +++ b/src/Gateway/Link.php @@ -1,6 +1,6 @@ setTransaction($transaction); - $outgoing->setDirection(WalletStatementDirection::Outgoing); + $outgoing->setDirection(StatementDirection::Outgoing); $outgoing = $this->wallet->spend($transaction); } diff --git a/src/Library/Economy/Payment/WalletGatewayService.php b/src/Gateway/Wallet/WalletService.php similarity index 90% rename from src/Library/Economy/Payment/WalletGatewayService.php rename to src/Gateway/Wallet/WalletService.php index ffc7ddd..30f3518 100644 --- a/src/Library/Economy/Payment/WalletGatewayService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -1,18 +1,17 @@ money->toBrick($statement->getTransaction()->getMoney()); switch ($statement->getDirection()) { - case WalletStatementDirection::Incoming: + case StatementDirection::Incoming: $total = $total->plus($transacted); break; - case WalletStatementDirection::Outgoing: + case StatementDirection::Outgoing: $total = $total->minus($transacted); break; } @@ -60,7 +59,7 @@ class WalletGatewayService { $statement = new WalletStatement(); $statement->setTransaction($transaction); - $statement->setDirection(WalletStatementDirection::Incoming); + $statement->setDirection(StatementDirection::Incoming); $statement->setBalance($transaction->getMoney()); $this->entityManager->persist($statement); @@ -85,7 +84,7 @@ class WalletGatewayService $outgoing = new WalletStatement(); $outgoing->setTransaction($transaction); - $outgoing->setDirection(WalletStatementDirection::Outgoing); + $outgoing->setDirection(StatementDirection::Outgoing); $incomings = $this->getStatements($origin); foreach ($incomings as $incoming) { diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 5994973..ceaa92b 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -3,20 +3,20 @@ namespace App\Library\Benzina\Pump; use App\Entity\Gateway\Charge; -use App\Entity\Gateway\ChargeType; use App\Entity\Gateway\Checkout; -use App\Entity\Gateway\CheckoutStatus; -use App\Entity\Gateway\Tracking; use App\Entity\Money; use App\Entity\Tipjar; +use App\Gateway\ChargeType; +use App\Gateway\CheckoutStatus; +use App\Gateway\Gateway\CashGateway; +use App\Gateway\Gateway\CecaGateway; +use App\Gateway\Gateway\DropGateway; +use App\Gateway\Gateway\PaypalGateway; +use App\Gateway\Stripe\StripeGateway; +use App\Gateway\Tracking; +use App\Gateway\Wallet\WalletGateway; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; -use App\Library\Economy\Payment\CashGateway; -use App\Library\Economy\Payment\CecaGateway; -use App\Library\Economy\Payment\DropGateway; -use App\Library\Economy\Payment\PaypalGateway; -use App\Library\Economy\Payment\StripeGateway; -use App\Library\Economy\Payment\WalletGateway; use App\Repository\ProjectRepository; use App\Repository\TipjarRepository; use App\Repository\UserRepository; diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php similarity index 87% rename from src/State/GatewayCheckoutProcessor.php rename to src/State/Gateway/CheckoutStateProcessor.php index 93e4de8..f45e42e 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -1,14 +1,14 @@ gateways->getGateways() as $gateway) { - $gateways[] = new GatewayApiResource($gateway); + $gateways[] = new Gateway($gateway); } return $gateways; } - private function getGateway(string $name): GatewayApiResource + private function getGateway(string $name): Gateway { try { $gateway = $this->gateways->getGateway($name); - return new GatewayApiResource($gateway); + return new Gateway($gateway); } catch (\Exception $e) { throw new NotFoundHttpException('Not Found'); } diff --git a/src/Validator/GatewayName.php b/src/Validator/GatewayName.php index b3350ce..8fbe6f7 100644 --- a/src/Validator/GatewayName.php +++ b/src/Validator/GatewayName.php @@ -2,7 +2,7 @@ namespace App\Validator; -use App\Library\Economy\Payment\GatewayLocator; +use App\Gateway\GatewayLocator; use Symfony\Component\Validator\Constraints\Choice; /** diff --git a/src/Validator/GatewayNameValidator.php b/src/Validator/GatewayNameValidator.php index 08393e0..c7f67d7 100644 --- a/src/Validator/GatewayNameValidator.php +++ b/src/Validator/GatewayNameValidator.php @@ -2,7 +2,7 @@ namespace App\Validator; -use App\Library\Economy\Payment\GatewayLocator; +use App\Gateway\GatewayLocator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\ChoiceValidator; diff --git a/src/Validator/SupportedChargeTypesValidator.php b/src/Validator/SupportedChargeTypesValidator.php index 0033ab3..ad3be30 100644 --- a/src/Validator/SupportedChargeTypesValidator.php +++ b/src/Validator/SupportedChargeTypesValidator.php @@ -2,7 +2,7 @@ namespace App\Validator; -use App\Library\Economy\Payment\GatewayLocator; +use App\Gateway\GatewayLocator; use Doctrine\Common\Collections\Collection; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; diff --git a/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php b/tests/Gateway/Paypal/PaypalServiceTest.php similarity index 86% rename from tests/Library/Economy/Payment/PaypalGatewayServiceTest.php rename to tests/Gateway/Paypal/PaypalServiceTest.php index 618282b..afbbec9 100644 --- a/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php +++ b/tests/Gateway/Paypal/PaypalServiceTest.php @@ -1,21 +1,21 @@ paypalService = static::getContainer()->get(PaypalGatewayService::class); + $this->paypalService = static::getContainer()->get(PaypalService::class); } public function testAuthenticates(): void diff --git a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php similarity index 85% rename from tests/Library/Economy/Payment/WalletGatewayServiceTest.php rename to tests/Gateway/Wallet/WalletServiceTest.php index 78c0f74..41b798a 100644 --- a/tests/Library/Economy/Payment/WalletGatewayServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -1,30 +1,30 @@ entityManager = static::getContainer()->get(EntityManagerInterface::class); - $this->walletService = static::getContainer()->get(WalletGatewayService::class); + $this->walletService = static::getContainer()->get(WalletService::class); } private function getUser(): User @@ -90,7 +90,7 @@ class WalletGatewayServiceTest extends KernelTestCase $statement = $statements[0]; $this->assertEquals(100, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); @@ -127,14 +127,14 @@ class WalletGatewayServiceTest extends KernelTestCase $statement = $statements[0]; $this->assertEquals(80, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; $this->assertEquals(20, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); @@ -157,21 +157,21 @@ class WalletGatewayServiceTest extends KernelTestCase $statement = $statements[0]; $this->assertEquals(50, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(2, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; $this->assertEquals(20, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $statement = $statements[2]; $this->assertEquals(30, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); @@ -224,28 +224,28 @@ class WalletGatewayServiceTest extends KernelTestCase $statement = $statements[0]; $this->assertEquals(0, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; $this->assertEquals(5, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[2]; $this->assertEquals(15, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Outgoing->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(2, $statement->getFinancedBy()); $statement = $statements[3]; $this->assertEquals(5, $statement->getBalance()->amount); - $this->assertEquals(WalletStatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); From 90767e2912b8b888c8ccf0fd91a16144c441701d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 10:16:37 +0100 Subject: [PATCH 61/86] Update Accounting api resource I/O --- src/ApiResource/Accounting/Accounting.php | 30 +++---------------- .../Accounting/AccountingStateProcessor.php | 14 ++++----- .../Accounting/AccountingStateProvider.php | 24 ++++++++++----- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php index 127decb..93dae33 100644 --- a/src/ApiResource/Accounting/Accounting.php +++ b/src/ApiResource/Accounting/Accounting.php @@ -6,7 +6,6 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\Entity\Accounting as Entity; use App\Entity\Interface\AccountingOwnerInterface; -use App\Entity\Interface\ApiResource; use App\State\Accounting\AccountingStateProcessor; use App\State\Accounting\AccountingStateProvider; @@ -19,7 +18,7 @@ use App\State\Accounting\AccountingStateProvider; */ #[API\ApiResource( shortName: 'Accounting', - stateOptions: new Options(entityClass: Accounting::class) + stateOptions: new Options(entityClass: Entity\Accounting::class) )] #[API\GetCollection(provider: AccountingStateProvider::class)] #[API\Get(provider: AccountingStateProvider::class)] @@ -29,30 +28,9 @@ use App\State\Accounting\AccountingStateProvider; )] class Accounting { - public function __construct( - private readonly Entity\Accounting $accounting, - private readonly AccountingOwnerInterface $owner, - ) {} + public ?int $id = null; - public function getId(): ?int - { - return $this->accounting->getId(); - } + public ?string $currency = null; - public function getCurrency(): ?string - { - return $this->accounting->getCurrency(); - } - - public function setCurrency(string $currency): static - { - $this->accounting->setCurrency($currency); - - return $this; - } - - public function getOwner(): ApiResource - { - return $this->owner; - } + public ?AccountingOwnerInterface $owner = null; } diff --git a/src/State/Accounting/AccountingStateProcessor.php b/src/State/Accounting/AccountingStateProcessor.php index 1645b65..9e22d25 100644 --- a/src/State/Accounting/AccountingStateProcessor.php +++ b/src/State/Accounting/AccountingStateProcessor.php @@ -4,8 +4,8 @@ namespace App\State\Accounting; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; -use App\ApiResource\AccountingApiResource; -use App\Entity\Accounting\Accounting; +use App\ApiResource\Accounting as ApiResource; +use App\Entity\Accounting as Entity; use Doctrine\ORM\EntityManagerInterface; class AccountingStateProcessor implements ProcessorInterface @@ -15,21 +15,21 @@ class AccountingStateProcessor implements ProcessorInterface ) {} /** - * @param AccountingApiResource $data + * @param ApiResource\Accounting $data * - * @return AccountingApiResource + * @return ApiResource\Accounting */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { /** @var Accounting */ - $accounting = $this->entityManager->find(Accounting::class, $data->getId()); - $accounting->setCurrency($data->getCurrency()); + $accounting = $this->entityManager->find(Entity\Accounting::class, $data->id); + $accounting->setCurrency($data->currency); $this->entityManager->persist($accounting); $this->entityManager->flush(); $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); - return new AccountingApiResource($accounting, $owner); + return new ApiResource\Accounting($accounting, $owner); } } diff --git a/src/State/Accounting/AccountingStateProvider.php b/src/State/Accounting/AccountingStateProvider.php index 718c19d..d41a37c 100644 --- a/src/State/Accounting/AccountingStateProvider.php +++ b/src/State/Accounting/AccountingStateProvider.php @@ -8,19 +8,19 @@ use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\Pagination\TraversablePaginator; use ApiPlatform\State\ProviderInterface; -use App\ApiResource\AccountingApiResource; -use App\Entity\Accounting\Accounting; +use App\ApiResource\Accounting as ApiResource; +use App\Entity\Accounting as Entity; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; class AccountingStateProvider implements ProviderInterface { public function __construct( - #[Autowire(service: CollectionProvider::class)] - private ProviderInterface $collectionProvider, + private EntityManagerInterface $entityManager, #[Autowire(service: ItemProvider::class)] private ProviderInterface $itemProvider, - private EntityManagerInterface $entityManager, + #[Autowire(service: CollectionProvider::class)] + private ProviderInterface $collectionProvider, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null @@ -46,14 +46,22 @@ class AccountingStateProvider implements ProviderInterface return $this->toResource($accounting); } - private function toResource(?Accounting $accounting): ?AccountingApiResource + private function toResource(?Entity\Accounting $accounting): ?ApiResource\Accounting { if ($accounting === null) { return null; } - $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); + $owner = $this->entityManager->find( + $accounting->getOwnerClass(), + $accounting->getOwnerId() + ); - return new AccountingApiResource($accounting, $owner); + $resource = new ApiResource\Accounting(); + $resource->id = $accounting->getId(); + $resource->currency = $accounting->getCurrency(); + $resource->owner = $owner; + + return $resource; } } From 391464d5ca7d59635eaa0677a4eeb1aa742edd2d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 14:14:09 +0100 Subject: [PATCH 62/86] Simplify Checkout entity --- src/Entity/Gateway/Checkout.php | 50 ++++++------------- src/Gateway/Paypal/PaypalGateway.php | 8 +-- src/Gateway/Stripe/StripeGateway.php | 4 +- src/Library/Benzina/Pump/CheckoutsPump.php | 4 +- src/Repository/Gateway/CheckoutRepository.php | 14 ++---- 5 files changed, 27 insertions(+), 53 deletions(-) diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 6832ffa..ae6beb1 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -89,7 +89,7 @@ class Checkout */ #[API\ApiProperty(writable: false)] #[ORM\Column] - private array $gatewayLinks = []; + private array $links = []; /** * A list of tracking codes provided by the Gateway for this checkout.\ @@ -99,7 +99,7 @@ class Checkout */ #[API\ApiProperty(writable: false)] #[ORM\Column] - private array $gatewayTrackings = []; + private array $trackings = []; /** * GatewayCheckout was migrated from an invest record in Goteo v3 platform. @@ -115,12 +115,6 @@ class Checkout #[ORM\Column(length: 255, nullable: true)] private ?string $migratedId = null; - /** - * A free-form collection of additional data associated with this checkout operation. - */ - #[ORM\Column(nullable: true)] - private ?array $metadata = null; - public function __construct() { $this->status = CheckoutStatus::Pending; @@ -201,22 +195,22 @@ class Checkout /** * @return Link[] */ - public function getGatewayLinks(): array + public function getLinks(): array { - return $this->gatewayLinks; + return $this->links; } - public function addGatewayLink(Link $link): static + public function addLink(Link $link): static { - $this->gatewayLinks = [...$this->gatewayLinks, $link]; + $this->links = [...$this->links, $link]; return $this; } - public function removeGatewayLink(Link $link): static + public function removeLink(Link $link): static { - $this->gatewayLinks = \array_filter( - $this->gatewayLinks, + $this->links = \array_filter( + $this->links, function (Link $existingLink) use ($link) { return $existingLink->href !== $link->href; } @@ -228,22 +222,22 @@ class Checkout /** * @return Tracking[] */ - public function getGatewayTrackings(): array + public function getTrackings(): array { - return $this->gatewayTrackings; + return $this->trackings; } - public function addGatewayTracking(Tracking $tracking): static + public function addTracking(Tracking $tracking): static { - $this->gatewayTrackings = [...$this->gatewayTrackings, $tracking]; + $this->trackings = [...$this->trackings, $tracking]; return $this; } - public function removeGatewayTracking(Tracking $tracking): static + public function removeTracking(Tracking $tracking): static { - $this->gatewayTrackings = \array_filter( - $this->gatewayTrackings, + $this->trackings = \array_filter( + $this->trackings, function (Tracking $existingTracking) use ($tracking) { return $existingTracking->title !== $tracking->title && $existingTracking->value !== $tracking->value; @@ -276,16 +270,4 @@ class Checkout return $this; } - - public function getMetadata(): ?array - { - return $this->metadata; - } - - public function setMetadata(?array $metadata): static - { - $this->metadata = $metadata; - - return $this; - } } diff --git a/src/Gateway/Paypal/PaypalGateway.php b/src/Gateway/Paypal/PaypalGateway.php index fb9335c..41ec5a8 100644 --- a/src/Gateway/Paypal/PaypalGateway.php +++ b/src/Gateway/Paypal/PaypalGateway.php @@ -75,7 +75,7 @@ class PaypalGateway implements GatewayInterface $tracking->title = self::TRACKING_TITLE_ORDER; $tracking->value = $order['id']; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); foreach ($order['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) @@ -88,7 +88,7 @@ class PaypalGateway implements GatewayInterface $link->method = $linkData['method']; $link->type = $linkType; - $checkout->addGatewayLink($link); + $checkout->addLink($link); } return $checkout; @@ -131,7 +131,7 @@ class PaypalGateway implements GatewayInterface $tracking->title = self::TRACKING_TITLE_TRANSACTION; $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $checkout = $this->checkoutService->chargeCheckout($checkout); @@ -181,7 +181,7 @@ class PaypalGateway implements GatewayInterface $tracking->title = self::TRACKING_TITLE_TRANSACTION; $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $checkout = $this->checkoutService->chargeCheckout($checkout); diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 4c69296..946c4e1 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -76,13 +76,13 @@ class StripeGateway implements GatewayInterface $link->method = Request::METHOD_GET; $link->type = LinkType::Payment; - $checkout->addGatewayLink($link); + $checkout->addLink($link); $tracking = new Tracking(); $tracking->title = self::TRACKING_TITLE_CHECKOUT; $tracking->value = $session->id; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); return $checkout; } diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index ceaa92b..e7d461e 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -11,7 +11,7 @@ use App\Gateway\CheckoutStatus; use App\Gateway\Gateway\CashGateway; use App\Gateway\Gateway\CecaGateway; use App\Gateway\Gateway\DropGateway; -use App\Gateway\Gateway\PaypalGateway; +use App\Gateway\Paypal\PaypalGateway; use App\Gateway\Stripe\StripeGateway; use App\Gateway\Tracking; use App\Gateway\Wallet\WalletGateway; @@ -120,7 +120,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->setGateway($this->getCheckoutGateway($record)); foreach ($this->getCheckoutTrackings($record) as $tracking) { - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $checkout->setMigrated(true); diff --git a/src/Repository/Gateway/CheckoutRepository.php b/src/Repository/Gateway/CheckoutRepository.php index ad73efa..5a587c2 100644 --- a/src/Repository/Gateway/CheckoutRepository.php +++ b/src/Repository/Gateway/CheckoutRepository.php @@ -4,7 +4,6 @@ namespace App\Repository\Gateway; use App\Entity\Gateway\Checkout; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\Query\Expr\Join; use Doctrine\Persistence\ManagerRegistry; /** @@ -25,17 +24,10 @@ class CheckoutRepository extends ServiceEntityRepository /** * @return Checkout|null */ - public function findOneByTracking(string $title, string $value): array + public function findOneByTracking(string $title, string $value): ?array { - return $this->createQueryBuilder('g') - ->join('g.gatewayTrackings', 'gt', Join::WITH, 'gt.checkout = g.id') - ->andWhere('gt.value = :val') - ->andWhere('gt.title = :title') - ->setParameter('val', $value) - ->setParameter('title', $title) - ->getQuery() - ->getOneOrNullResult() - ; + // TO-DO: Use a JSON lookup into the 'trackings' column to see if it contains title and value + return null; } // /** From 4a97a6277cec76163c432bbdace39206a4977150 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 14:22:11 +0100 Subject: [PATCH 63/86] Refactor Gateway api resource provision --- src/ApiResource/Gateway/Gateway.php | 17 ++----- src/State/Gateway/GatewayStateProvider.php | 52 +++++++++++++--------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/ApiResource/Gateway/Gateway.php b/src/ApiResource/Gateway/Gateway.php index 795cb14..6f9fbd7 100644 --- a/src/ApiResource/Gateway/Gateway.php +++ b/src/ApiResource/Gateway/Gateway.php @@ -3,7 +3,6 @@ namespace App\ApiResource\Gateway; use ApiPlatform\Metadata as API; -use App\Gateway\GatewayInterface; use App\State\Gateway\GatewayStateProvider; /** @@ -18,21 +17,11 @@ use App\State\Gateway\GatewayStateProvider; #[API\Get(provider: GatewayStateProvider::class)] class Gateway { - public function __construct(private readonly GatewayInterface $gateway) {} - #[API\ApiProperty(identifier: true)] - public function getName(): string - { - return $this->gateway->getName(); - } + public ?string $name = null; /** - * The GatewayCharge types that can be processed by this Gateway. - * - * @return \App\Gateway\ChargeType[] + * @var array */ - public function getSupported(): array - { - return $this->gateway->getSupportedChargeTypes(); - } + public ?array $supports = null; } diff --git a/src/State/Gateway/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php index 7d3ffe6..064c877 100644 --- a/src/State/Gateway/GatewayStateProvider.php +++ b/src/State/Gateway/GatewayStateProvider.php @@ -6,6 +6,7 @@ use ApiPlatform\Metadata as API; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Gateway\Gateway; +use App\Gateway\GatewayInterface; use App\Gateway\GatewayLocator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -15,27 +16,6 @@ class GatewayStateProvider implements ProviderInterface private GatewayLocator $gateways, ) {} - private function getGateways(): array - { - $gateways = []; - foreach ($this->gateways->getGateways() as $gateway) { - $gateways[] = new Gateway($gateway); - } - - return $gateways; - } - - private function getGateway(string $name): Gateway - { - try { - $gateway = $this->gateways->getGateway($name); - - return new Gateway($gateway); - } catch (\Exception $e) { - throw new NotFoundHttpException('Not Found'); - } - } - public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { switch ($operation::class) { @@ -45,4 +25,34 @@ class GatewayStateProvider implements ProviderInterface return $this->getGateway($uriVariables['name']); } } + + private function toResource(GatewayInterface $gateway): Gateway + { + $resource = new Gateway(); + $resource->name = $gateway::getName(); + $resource->supports = $gateway::getSupportedChargeTypes(); + + return $resource; + } + + private function getGateways(): array + { + $gateways = []; + foreach ($this->gateways->getGateways() as $gateway) { + $gateways[] = $this->toResource($gateway); + } + + return $gateways; + } + + private function getGateway(string $name): Gateway + { + try { + $gateway = $this->gateways->getGateway($name); + + return $this->toResource($gateway); + } catch (\Exception $e) { + throw new NotFoundHttpException('Not Found'); + } + } } From ebde320e809583b33c3902410fce748ce3bb7c9e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 14:27:05 +0100 Subject: [PATCH 64/86] Simplify AccountingOwnerInterface --- src/ApiResource/Accounting/Accounting.php | 11 +++++------ src/Entity/Interface/AccountingOwnerInterface.php | 2 +- src/Entity/Interface/ApiResource.php | 8 -------- 3 files changed, 6 insertions(+), 15 deletions(-) delete mode 100644 src/Entity/Interface/ApiResource.php diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php index 93dae33..2f85039 100644 --- a/src/ApiResource/Accounting/Accounting.php +++ b/src/ApiResource/Accounting/Accounting.php @@ -18,14 +18,13 @@ use App\State\Accounting\AccountingStateProvider; */ #[API\ApiResource( shortName: 'Accounting', - stateOptions: new Options(entityClass: Entity\Accounting::class) -)] -#[API\GetCollection(provider: AccountingStateProvider::class)] -#[API\Get(provider: AccountingStateProvider::class)] -#[API\Patch( + stateOptions: new Options(entityClass: Entity\Accounting::class), + provider: AccountingStateProvider::class, processor: AccountingStateProcessor::class, - security: 'is_granted("ACCOUNTING_EDIT", object)' )] +#[API\GetCollection()] +#[API\Get()] +#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')] class Accounting { public ?int $id = null; diff --git a/src/Entity/Interface/AccountingOwnerInterface.php b/src/Entity/Interface/AccountingOwnerInterface.php index ddb90ef..35da0b2 100644 --- a/src/Entity/Interface/AccountingOwnerInterface.php +++ b/src/Entity/Interface/AccountingOwnerInterface.php @@ -4,7 +4,7 @@ namespace App\Entity\Interface; use App\Entity\Accounting\Accounting; -interface AccountingOwnerInterface extends ApiResource +interface AccountingOwnerInterface { public function getId(): ?int; diff --git a/src/Entity/Interface/ApiResource.php b/src/Entity/Interface/ApiResource.php deleted file mode 100644 index ebfebe1..0000000 --- a/src/Entity/Interface/ApiResource.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Wed, 6 Nov 2024 14:29:17 +0100 Subject: [PATCH 65/86] Rename Timestampable* traits to Timestamped --- src/Entity/Gateway/Checkout.php | 8 ++++---- src/Entity/Project.php | 8 ++++---- ...leCreationEntity.php => TimestampedCreationEntity.php} | 2 +- ...leUpdationEntity.php => TimestampedUpdationEntity.php} | 2 +- src/Entity/User.php | 8 ++++---- src/Entity/UserToken.php | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) rename src/Entity/Trait/{TimestampableCreationEntity.php => TimestampedCreationEntity.php} (95%) rename src/Entity/Trait/{TimestampableUpdationEntity.php => TimestampedUpdationEntity.php} (95%) diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index ae6beb1..8405608 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -5,8 +5,8 @@ namespace App\Entity\Gateway; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; -use App\Entity\Trait\TimestampableCreationEntity; -use App\Entity\Trait\TimestampableUpdationEntity; +use App\Entity\Trait\TimestampedCreationEntity; +use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\Tracking; @@ -35,8 +35,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(fields: ['migratedId'])] class Checkout { - use TimestampableCreationEntity; - use TimestampableUpdationEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 201e706..15ddad7 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -6,8 +6,8 @@ use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; -use App\Entity\Trait\TimestampableCreationEntity; -use App\Entity\Trait\TimestampableUpdationEntity; +use App\Entity\Trait\TimestampedCreationEntity; +use App\Entity\Trait\TimestampedUpdationEntity; use App\Repository\ProjectRepository; use Doctrine\ORM\Mapping as ORM; @@ -27,8 +27,8 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project implements AccountingOwnerInterface { - use TimestampableCreationEntity; - use TimestampableUpdationEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Entity/Trait/TimestampableCreationEntity.php b/src/Entity/Trait/TimestampedCreationEntity.php similarity index 95% rename from src/Entity/Trait/TimestampableCreationEntity.php rename to src/Entity/Trait/TimestampedCreationEntity.php index d5d7e16..ba3be09 100644 --- a/src/Entity/Trait/TimestampableCreationEntity.php +++ b/src/Entity/Trait/TimestampedCreationEntity.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; -trait TimestampableCreationEntity +trait TimestampedCreationEntity { /** * Entity creation timestamp. diff --git a/src/Entity/Trait/TimestampableUpdationEntity.php b/src/Entity/Trait/TimestampedUpdationEntity.php similarity index 95% rename from src/Entity/Trait/TimestampableUpdationEntity.php rename to src/Entity/Trait/TimestampedUpdationEntity.php index baafdf3..ccf46a1 100644 --- a/src/Entity/Trait/TimestampableUpdationEntity.php +++ b/src/Entity/Trait/TimestampedUpdationEntity.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; -trait TimestampableUpdationEntity +trait TimestampedUpdationEntity { /** * Entity last update timestamp. diff --git a/src/Entity/User.php b/src/Entity/User.php index 414130d..721edb1 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -6,8 +6,8 @@ use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\UserOwnedInterface; -use App\Entity\Trait\TimestampableCreationEntity; -use App\Entity\Trait\TimestampableUpdationEntity; +use App\Entity\Trait\TimestampedCreationEntity; +use App\Entity\Trait\TimestampedUpdationEntity; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; use App\Repository\UserRepository; @@ -42,8 +42,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(fields: ['migratedId'])] class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { - use TimestampableCreationEntity; - use TimestampableUpdationEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Entity/UserToken.php b/src/Entity/UserToken.php index 516bcda..9a20493 100644 --- a/src/Entity/UserToken.php +++ b/src/Entity/UserToken.php @@ -5,7 +5,7 @@ namespace App\Entity; use ApiPlatform\Metadata as API; use App\Dto\UserTokenLoginDto; use App\Entity\Interface\UserOwnedInterface; -use App\Entity\Trait\TimestampableCreationEntity; +use App\Entity\Trait\TimestampedCreationEntity; use App\Repository\UserTokenRepository; use App\State\UserTokenLoginProcessor; use Doctrine\ORM\Mapping as ORM; @@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: UserTokenRepository::class)] class UserToken implements UserOwnedInterface { - use TimestampableCreationEntity; + use TimestampedCreationEntity; #[ORM\Id] #[ORM\GeneratedValue] From d6668a29be53598ecd8d25975bebf49f036281b5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 14:45:08 +0100 Subject: [PATCH 66/86] Standarise entity migration metadata via trait --- src/Entity/Gateway/Checkout.php | 40 ++---------------------- src/Entity/Project.php | 45 ++------------------------- src/Entity/Trait/MigratedEntity.php | 47 +++++++++++++++++++++++++++++ src/Entity/User.php | 41 ++----------------------- 4 files changed, 53 insertions(+), 120 deletions(-) create mode 100644 src/Entity/Trait/MigratedEntity.php diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 8405608..869cf91 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -5,6 +5,7 @@ namespace App\Entity\Gateway; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; +use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\CheckoutStatus; @@ -35,6 +36,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(fields: ['migratedId'])] class Checkout { + use MigratedEntity; use TimestampedCreationEntity; use TimestampedUpdationEntity; @@ -101,20 +103,6 @@ class Checkout #[ORM\Column] private array $trackings = []; - /** - * GatewayCheckout was migrated from an invest record in Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column] - private ?bool $migrated = false; - - /** - * The id of the original invest record in the Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedId = null; - public function __construct() { $this->status = CheckoutStatus::Pending; @@ -246,28 +234,4 @@ class Checkout return $this; } - - public function isMigrated(): ?bool - { - return $this->migrated; - } - - public function setMigrated(bool $migrated): static - { - $this->migrated = $migrated; - - return $this; - } - - public function getMigratedId(): ?string - { - return $this->migratedId; - } - - public function setMigratedId(?string $migratedId): static - { - $this->migratedId = $migratedId; - - return $this; - } } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 15ddad7..cb8ef41 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -6,6 +6,7 @@ use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Repository\ProjectRepository; @@ -27,6 +28,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project implements AccountingOwnerInterface { + use MigratedEntity; use TimestampedCreationEntity; use TimestampedUpdationEntity; @@ -65,25 +67,6 @@ class Project implements AccountingOwnerInterface #[ORM\Column(type: 'string', enumType: ProjectStatus::class)] private ProjectStatus $status; - /** - * Project was migrated from Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column] - private ?bool $migrated = null; - - /** - * The previous id of this Project in the Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedId = null; - - public function __construct() - { - $this->migrated = false; - } - public function getId(): ?int { return $this->id; @@ -136,28 +119,4 @@ class Project implements AccountingOwnerInterface return $this; } - - public function isMigrated(): ?bool - { - return $this->migrated; - } - - public function setMigrated(bool $migrated): static - { - $this->migrated = $migrated; - - return $this; - } - - public function getMigratedId(): ?string - { - return $this->migratedId; - } - - public function setMigratedId(?string $migratedId): static - { - $this->migratedId = $migratedId; - - return $this; - } } diff --git a/src/Entity/Trait/MigratedEntity.php b/src/Entity/Trait/MigratedEntity.php new file mode 100644 index 0000000..07a3135 --- /dev/null +++ b/src/Entity/Trait/MigratedEntity.php @@ -0,0 +1,47 @@ +migrated; + } + + public function setMigrated(bool $migrated): static + { + $this->migrated = $migrated; + + return $this; + } + + public function getMigratedId(): ?string + { + return $this->migratedId; + } + + public function setMigratedId(string $migratedId): static + { + $this->migratedId = $migratedId; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 721edb1..703ae9a 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -6,6 +6,7 @@ use ApiPlatform\Metadata as API; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\UserOwnedInterface; +use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Filter\OrderedLikeFilter; @@ -42,6 +43,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(fields: ['migratedId'])] class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { + use MigratedEntity; use TimestampedCreationEntity; use TimestampedUpdationEntity; @@ -126,20 +128,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; - /** - * User was migrated from Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column] - private ?bool $migrated = null; - - /** - * The previous id of this User in the Goteo v3 platform. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedId = null; - /** * The projects owned by this User. */ @@ -154,7 +142,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs { $this->emailConfirmed = false; $this->active = false; - $this->migrated = false; $this->tokens = new ArrayCollection(); $this->projects = new ArrayCollection(); @@ -365,30 +352,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs return $this; } - public function isMigrated(): ?bool - { - return $this->migrated; - } - - public function setMigrated(bool $migrated): static - { - $this->migrated = $migrated; - - return $this; - } - - public function getMigratedId(): ?string - { - return $this->migratedId; - } - - public function setMigratedId(?string $migratedId): static - { - $this->migratedId = $migratedId; - - return $this; - } - /** * @return Collection */ From 25080c458223100f4fc17326a7dbe6b2b0ba0534 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 15:47:20 +0100 Subject: [PATCH 67/86] Move accounting entity<->resource mapping to dedicated class --- src/Mapping/Accounting/AccountingMapper.php | 45 +++++++++++++++++++ .../Accounting/AccountingStateProcessor.php | 20 ++++----- .../Accounting/AccountingStateProvider.php | 16 ++----- 3 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 src/Mapping/Accounting/AccountingMapper.php diff --git a/src/Mapping/Accounting/AccountingMapper.php b/src/Mapping/Accounting/AccountingMapper.php new file mode 100644 index 0000000..d6def27 --- /dev/null +++ b/src/Mapping/Accounting/AccountingMapper.php @@ -0,0 +1,45 @@ +entityManager->find( + $entity->getOwnerClass(), + $entity->getOwnerId() + ); + + $resource = new Resource\Accounting(); + $resource->id = $entity->getId(); + $resource->currency = $entity->getCurrency(); + $resource->owner = $owner; + + return $resource; + } + + public function toEntity(Resource\Accounting $resource): Entity\Accounting + { + $entity = new Entity\Accounting(); + + if ($resource->id !== null) { + $entity = $this->accountingRepository->find($resource->id); + } + + $entity->setCurrency($resource->currency); + $entity->setOwner($resource->owner); + + return $entity; + } +} diff --git a/src/State/Accounting/AccountingStateProcessor.php b/src/State/Accounting/AccountingStateProcessor.php index 9e22d25..12fc107 100644 --- a/src/State/Accounting/AccountingStateProcessor.php +++ b/src/State/Accounting/AccountingStateProcessor.php @@ -2,16 +2,20 @@ namespace App\State\Accounting; +use ApiPlatform\Doctrine\Common\State\PersistProcessor; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Accounting as ApiResource; use App\Entity\Accounting as Entity; -use Doctrine\ORM\EntityManagerInterface; +use App\Mapping\Accounting\AccountingMapper; +use Symfony\Component\DependencyInjection\Attribute\Autowire; class AccountingStateProcessor implements ProcessorInterface { public function __construct( - private EntityManagerInterface $entityManager, + private AccountingMapper $accountingMapper, + #[Autowire(service: PersistProcessor::class)] + private ProcessorInterface $persistProcessor, ) {} /** @@ -21,15 +25,11 @@ class AccountingStateProcessor implements ProcessorInterface */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - /** @var Accounting */ - $accounting = $this->entityManager->find(Entity\Accounting::class, $data->id); - $accounting->setCurrency($data->currency); + /** @var Entity\Accounting */ + $entity = $this->accountingMapper->toEntity($data); - $this->entityManager->persist($accounting); - $this->entityManager->flush(); + $this->persistProcessor->process($entity, $operation, $uriVariables, $context); - $owner = $this->entityManager->find($accounting->getOwnerClass(), $accounting->getOwnerId()); - - return new ApiResource\Accounting($accounting, $owner); + return $this->accountingMapper->toResource($entity); } } diff --git a/src/State/Accounting/AccountingStateProvider.php b/src/State/Accounting/AccountingStateProvider.php index d41a37c..3982a52 100644 --- a/src/State/Accounting/AccountingStateProvider.php +++ b/src/State/Accounting/AccountingStateProvider.php @@ -10,17 +10,17 @@ use ApiPlatform\State\Pagination\TraversablePaginator; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Accounting as ApiResource; use App\Entity\Accounting as Entity; -use Doctrine\ORM\EntityManagerInterface; +use App\Mapping\Accounting\AccountingMapper; use Symfony\Component\DependencyInjection\Attribute\Autowire; class AccountingStateProvider implements ProviderInterface { public function __construct( - private EntityManagerInterface $entityManager, #[Autowire(service: ItemProvider::class)] private ProviderInterface $itemProvider, #[Autowire(service: CollectionProvider::class)] private ProviderInterface $collectionProvider, + private AccountingMapper $accountingMapper, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null @@ -52,16 +52,6 @@ class AccountingStateProvider implements ProviderInterface return null; } - $owner = $this->entityManager->find( - $accounting->getOwnerClass(), - $accounting->getOwnerId() - ); - - $resource = new ApiResource\Accounting(); - $resource->id = $accounting->getId(); - $resource->currency = $accounting->getCurrency(); - $resource->owner = $owner; - - return $resource; + return $this->accountingMapper->toResource($accounting); } } From a980129bae9d065ddd4b7946eb1184a21d1739f3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 6 Nov 2024 15:49:43 +0100 Subject: [PATCH 68/86] Dedicated api resource for charge --- src/ApiResource/Gateway/Charge.php | 61 +++++++++++++++++++++++ src/Entity/Gateway/Charge.php | 4 -- src/Mapping/Gateway/ChargeMapper.php | 27 ++++++++++ src/State/Gateway/ChargeStateProvider.php | 29 +++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/ApiResource/Gateway/Charge.php create mode 100644 src/Mapping/Gateway/ChargeMapper.php create mode 100644 src/State/Gateway/ChargeStateProvider.php diff --git a/src/ApiResource/Gateway/Charge.php b/src/ApiResource/Gateway/Charge.php new file mode 100644 index 0000000..13fb4b6 --- /dev/null +++ b/src/ApiResource/Gateway/Charge.php @@ -0,0 +1,61 @@ + */ - #[API\ApiProperty(readableLink: false, writable: false)] #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; diff --git a/src/Mapping/Gateway/ChargeMapper.php b/src/Mapping/Gateway/ChargeMapper.php new file mode 100644 index 0000000..81963e6 --- /dev/null +++ b/src/Mapping/Gateway/ChargeMapper.php @@ -0,0 +1,27 @@ +id = $entity->getId(); + $resource->type = $entity->getType(); + $resource->title = $entity->getTitle(); + $resource->description = $entity->getDescription(); + $resource->money = $entity->getMoney(); + $resource->target = $this->accountingMapper->toResource($entity->getTarget()); + + return $resource; + } +} diff --git a/src/State/Gateway/ChargeStateProvider.php b/src/State/Gateway/ChargeStateProvider.php new file mode 100644 index 0000000..5aad8f4 --- /dev/null +++ b/src/State/Gateway/ChargeStateProvider.php @@ -0,0 +1,29 @@ +itemProvider->provide($operation, $uriVariables, $context); + + if ($charge === null) { + return null; + } + + return $this->chargeMapper->toResource($charge); + } +} From 3a840e53cc0d4910cd873658194673bcd177e39c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 10:30:49 +0100 Subject: [PATCH 69/86] Add Gateway mapping --- src/ApiResource/Gateway/Charge.php | 2 +- src/Mapping/Accounting/AccountingMapper.php | 2 +- src/Mapping/Gateway/GatewayMapper.php | 18 ++++++++++++++++++ src/State/Gateway/GatewayStateProvider.php | 16 ++++------------ 4 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 src/Mapping/Gateway/GatewayMapper.php diff --git a/src/ApiResource/Gateway/Charge.php b/src/ApiResource/Gateway/Charge.php index 13fb4b6..0979fe2 100644 --- a/src/ApiResource/Gateway/Charge.php +++ b/src/ApiResource/Gateway/Charge.php @@ -53,7 +53,7 @@ class Charge /** * The money to-be-paid at the gateway. - * + * * It is money before gateway fees and taxes, not accountable. */ #[Assert\NotBlank()] diff --git a/src/Mapping/Accounting/AccountingMapper.php b/src/Mapping/Accounting/AccountingMapper.php index d6def27..7c3454f 100644 --- a/src/Mapping/Accounting/AccountingMapper.php +++ b/src/Mapping/Accounting/AccountingMapper.php @@ -11,7 +11,7 @@ class AccountingMapper { public function __construct( private EntityManagerInterface $entityManager, - private AccountingRepository $accountingRepository + private AccountingRepository $accountingRepository, ) {} public function toResource(Entity\Accounting $entity): Resource\Accounting diff --git a/src/Mapping/Gateway/GatewayMapper.php b/src/Mapping/Gateway/GatewayMapper.php new file mode 100644 index 0000000..16e06a7 --- /dev/null +++ b/src/Mapping/Gateway/GatewayMapper.php @@ -0,0 +1,18 @@ +name = $gateway::getName(); + $resource->supports = $gateway::getSupportedChargeTypes(); + + return $resource; + } +} diff --git a/src/State/Gateway/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php index 064c877..803a7e9 100644 --- a/src/State/Gateway/GatewayStateProvider.php +++ b/src/State/Gateway/GatewayStateProvider.php @@ -6,14 +6,15 @@ use ApiPlatform\Metadata as API; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Gateway\Gateway; -use App\Gateway\GatewayInterface; use App\Gateway\GatewayLocator; +use App\Mapping\Gateway\GatewayMapper; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class GatewayStateProvider implements ProviderInterface { public function __construct( private GatewayLocator $gateways, + private GatewayMapper $gatewayMapper, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null @@ -26,20 +27,11 @@ class GatewayStateProvider implements ProviderInterface } } - private function toResource(GatewayInterface $gateway): Gateway - { - $resource = new Gateway(); - $resource->name = $gateway::getName(); - $resource->supports = $gateway::getSupportedChargeTypes(); - - return $resource; - } - private function getGateways(): array { $gateways = []; foreach ($this->gateways->getGateways() as $gateway) { - $gateways[] = $this->toResource($gateway); + $gateways[] = $this->gatewayMapper->toResource($gateway); } return $gateways; @@ -50,7 +42,7 @@ class GatewayStateProvider implements ProviderInterface try { $gateway = $this->gateways->getGateway($name); - return $this->toResource($gateway); + return $this->gatewayMapper->toResource($gateway); } catch (\Exception $e) { throw new NotFoundHttpException('Not Found'); } From 5d24dfaac3a6341caecd828651714a6327b4e539 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 11:41:40 +0100 Subject: [PATCH 70/86] Allow to map charge to entity --- src/Mapping/Gateway/ChargeMapper.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Mapping/Gateway/ChargeMapper.php b/src/Mapping/Gateway/ChargeMapper.php index 81963e6..d74fc63 100644 --- a/src/Mapping/Gateway/ChargeMapper.php +++ b/src/Mapping/Gateway/ChargeMapper.php @@ -5,11 +5,13 @@ namespace App\Mapping\Gateway; use App\ApiResource\Gateway as Resource; use App\Entity\Gateway as Entity; use App\Mapping\Accounting\AccountingMapper; +use App\Repository\Gateway\ChargeRepository; class ChargeMapper { public function __construct( private AccountingMapper $accountingMapper, + private ChargeRepository $chargeRepository, ) {} public function toResource(Entity\Charge $entity): Resource\Charge @@ -24,4 +26,21 @@ class ChargeMapper return $resource; } + + public function toEntity(Resource\Charge $resource): Entity\Charge + { + $entity = new Entity\Charge(); + + if ($resource->id !== null) { + $entity = $this->chargeRepository->find($resource->id); + } + + $entity->setType($resource->type); + $entity->setTitle($resource->title); + $entity->setDescription($resource->description); + $entity->setMoney($resource->money); + $entity->setTarget($this->accountingMapper->toEntity($resource->target)); + + return $entity; + } } From f9ea60290b6f8fd46dab0368c18027bbd6d957d2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 11:41:52 +0100 Subject: [PATCH 71/86] Update charge docblocks --- src/ApiResource/Gateway/Charge.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/ApiResource/Gateway/Charge.php b/src/ApiResource/Gateway/Charge.php index 0979fe2..7252f2b 100644 --- a/src/ApiResource/Gateway/Charge.php +++ b/src/ApiResource/Gateway/Charge.php @@ -26,35 +26,37 @@ class Charge public ?int $id = null; /** - * The type represents the kind of payment that the Gateway must process.\ + * How this item should be processed by the Gateway.\ * \ - * `single` is for one time payments. - * `recurring` is for subscription-based payments. + * `single` is for one time payments.\ + * `recurring` is for payments repeated over time. */ #[Assert\NotBlank()] - public ?ChargeType $type = ChargeType::Single; + public ChargeType $type = ChargeType::Single; /** - * A short, descriptive string for this charge item. May be displayed to the origin. + * A short, descriptive string for this charge item.\ + * May be displayed to the payer. */ #[Assert\NotBlank()] - public ?string $title = null; + public string $title; /** - * Detailed information about the charge item. May be displayed to the origin. + * Detailed information about the charge item.\ + * May be displayed to the payer. */ public ?string $description = null; /** - * The receiver of the successful payment. + * The Accounting receiving the money after a successful payment. */ #[Assert\NotBlank()] public Accounting $target; /** - * The money to-be-paid at the gateway. + * The money to-be-paid for this item at the Gateway. * - * It is money before gateway fees and taxes, not accountable. + * It is money before fees and taxes, not accountable. */ #[Assert\NotBlank()] public Money $money; From bd5aff193ff7185ca6ae448ccfa43224b7f3279a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 12:05:30 +0100 Subject: [PATCH 72/86] Process checkout as api resource --- src/ApiResource/Gateway/Checkout.php | 74 ++++++++++++++++++++ src/Entity/Gateway/Checkout.php | 48 +++++++++---- src/Mapping/Gateway/CheckoutMapper.php | 69 ++++++++++++++++++ src/State/Gateway/CheckoutStateProcessor.php | 20 +++--- 4 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 src/ApiResource/Gateway/Checkout.php create mode 100644 src/Mapping/Gateway/CheckoutMapper.php diff --git a/src/ApiResource/Gateway/Checkout.php b/src/ApiResource/Gateway/Checkout.php new file mode 100644 index 0000000..8fb7780 --- /dev/null +++ b/src/ApiResource/Gateway/Checkout.php @@ -0,0 +1,74 @@ + 'exact', 'charges.target' => 'exact'])] #[ORM\Entity(repositoryClass: CheckoutRepository::class)] #[ORM\Index(fields: ['migratedId'])] class Checkout @@ -57,8 +50,6 @@ class Checkout * The status of the checkout with the Gateway. */ #[Gedmo\Versioned] - #[API\ApiProperty(writable: false)] - #[API\ApiFilter(SearchFilter::class)] #[ORM\Column()] private ?CheckoutStatus $status = null; @@ -67,7 +58,6 @@ class Checkout * * @var Collection */ - #[API\ApiProperty(readableLink: true, writableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] #[SupportedChargeTypes()] @@ -79,7 +69,6 @@ class Checkout */ #[GatewayName] #[Assert\NotBlank()] - #[API\ApiFilter(SearchFilter::class)] #[ORM\Column(length: 255)] private ?string $gateway = null; @@ -89,7 +78,6 @@ class Checkout * * @var Link[] */ - #[API\ApiProperty(writable: false)] #[ORM\Column] private array $links = []; @@ -99,7 +87,6 @@ class Checkout * * @var Tracking[] */ - #[API\ApiProperty(writable: false)] #[ORM\Column] private array $trackings = []; @@ -146,6 +133,21 @@ class Checkout return $this->charges; } + /** + * @param Collection $charges + */ + public function setCharges(Collection $charges): static + { + $this->charges = new ArrayCollection(); + + foreach ($charges as $charge) { + $this->charges->add($charge); + $charge->setCheckout($this); + } + + return $this; + } + public function addCharge(Charge $charge): static { if (!$this->charges->contains($charge)) { @@ -188,6 +190,16 @@ class Checkout return $this->links; } + /** + * @param Link[] $links + */ + public function setLinks(array $links): static + { + $this->links = $links; + + return $this; + } + public function addLink(Link $link): static { $this->links = [...$this->links, $link]; @@ -215,6 +227,16 @@ class Checkout return $this->trackings; } + /** + * @param Tracking[] $trackings + */ + public function setTrackings(array $trackings): static + { + $this->trackings = $trackings; + + return $this; + } + public function addTracking(Tracking $tracking): static { $this->trackings = [...$this->trackings, $tracking]; diff --git a/src/Mapping/Gateway/CheckoutMapper.php b/src/Mapping/Gateway/CheckoutMapper.php new file mode 100644 index 0000000..c7f24b8 --- /dev/null +++ b/src/Mapping/Gateway/CheckoutMapper.php @@ -0,0 +1,69 @@ +id = $entity->getId(); + + $gateway = $this->gatewayLocator->getGateway($entity->getGateway()); + $resource->gateway = $this->gatewayMapper->toResource($gateway); + + $resource->origin = $this->accountingMapper->toResource($entity->getOrigin()); + + $charges = []; + foreach ($entity->getCharges() as $charge) { + $charges[] = $this->chargeMapper->toResource($charge); + } + + $resource->charges = $charges; + + $resource->status = $entity->getStatus(); + $resource->links = $entity->getLinks(); + $resource->trackings = $entity->getTrackings(); + + return $resource; + } + + public function toEntity(Resource\Checkout $resource): Entity\Checkout + { + $entity = new Entity\Checkout(); + + if ($resource->id !== null) { + $entity = $this->checkoutRepository->find($resource->id); + } + + $entity->setGateway($resource->gateway->name); + $entity->setOrigin($this->accountingMapper->toEntity($resource->origin)); + + $charges = []; + foreach ($resource->charges as $charge) { + $charges[] = $this->chargeMapper->toEntity($charge); + } + + $entity->setCharges(new ArrayCollection($charges)); + + $entity->setStatus($resource->status); + $entity->setLinks($resource->links); + + return $entity; + } +} diff --git a/src/State/Gateway/CheckoutStateProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php index f45e42e..f8d7509 100644 --- a/src/State/Gateway/CheckoutStateProcessor.php +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -2,32 +2,36 @@ namespace App\State\Gateway; +use ApiPlatform\Doctrine\Common\State\PersistProcessor; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; -use App\Entity\GatewayCheckout; +use App\ApiResource\Gateway as Resource; use App\Gateway\GatewayLocator; +use App\Mapping\Gateway\CheckoutMapper; use Symfony\Component\DependencyInjection\Attribute\Autowire; class CheckoutStateProcessor implements ProcessorInterface { public function __construct( - #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private CheckoutMapper $checkoutMapper, + #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, ) {} /** - * @param GatewayCheckout $data + * @param Resource\Checkout $data * - * @return GatewayCheckout + * @return Resource\Checkout */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - $checkout = $this->innerProcessor->process($data, $operation, $uriVariables, $context); + $entity = $this->checkoutMapper->toEntity($data); + $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); - $gateway = $this->gatewayLocator->getGatewayOf($checkout); - $checkout = $gateway->process($checkout); + $entity = $this->gatewayLocator->getGateway($data->gateway->name)->process($entity); + $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); - return $this->innerProcessor->process($checkout, $operation, $uriVariables, $context); + return $this->checkoutMapper->toResource($entity); } } From 9602028d1da560c6306fba6235775b79558aa507 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 12:05:46 +0100 Subject: [PATCH 73/86] Filter null values in product_data array --- src/Gateway/Stripe/StripeGateway.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 946c4e1..35a1c84 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -166,10 +166,10 @@ class StripeGateway implements GatewayInterface $price = [ 'currency' => $charge->getMoney()->currency, 'unit_amount' => $charge->getMoney()->amount, - 'product_data' => [ + 'product_data' => \array_filter([ 'name' => $charge->getTitle(), 'statement_descriptor' => $charge->getDescription(), - ], + ], fn ($v) => $v !== null), ]; if ($charge->getType() === ChargeType::Recurring) { From a3e5ffe65cd159313f89c52e7735cb8cb9b66b8e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 12:14:58 +0100 Subject: [PATCH 74/86] Remove GatewayName validation --- src/Entity/Gateway/Checkout.php | 2 -- src/Validator/GatewayName.php | 22 ---------------- src/Validator/GatewayNameValidator.php | 36 -------------------------- 3 files changed, 60 deletions(-) delete mode 100644 src/Validator/GatewayName.php delete mode 100644 src/Validator/GatewayNameValidator.php diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 8de6f76..6add15e 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -10,7 +10,6 @@ use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\Tracking; use App\Repository\Gateway\CheckoutRepository; -use App\Validator\GatewayName; use App\Validator\SupportedChargeTypes; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -67,7 +66,6 @@ class Checkout /** * The name of the Gateway implementation to checkout with. */ - #[GatewayName] #[Assert\NotBlank()] #[ORM\Column(length: 255)] private ?string $gateway = null; diff --git a/src/Validator/GatewayName.php b/src/Validator/GatewayName.php deleted file mode 100644 index 8fbe6f7..0000000 --- a/src/Validator/GatewayName.php +++ /dev/null @@ -1,22 +0,0 @@ -gateways->getGateway($value); - - return; - } catch (\Exception $e) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setParameter('{{ choices }}', $this->formatValues($constraint->choices)) - ->setCode(Choice::NO_SUCH_CHOICE_ERROR) - ->addViolation(); - } - } -} From 1c7c937729effd9059b11d3bfdc1be744ee52afa Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 12:15:35 +0100 Subject: [PATCH 75/86] Remove gateways static names compilation --- src/Command/GatewaysSetupCommand.php | 48 ----- .../Compiler/GatewaysCompilerPass.php | 171 ------------------ src/Kernel.php | 6 - 3 files changed, 225 deletions(-) delete mode 100644 src/Command/GatewaysSetupCommand.php delete mode 100755 src/DependencyInjection/Compiler/GatewaysCompilerPass.php diff --git a/src/Command/GatewaysSetupCommand.php b/src/Command/GatewaysSetupCommand.php deleted file mode 100644 index 97b3bb9..0000000 --- a/src/Command/GatewaysSetupCommand.php +++ /dev/null @@ -1,48 +0,0 @@ -gatewayLocator->getClasses(); - GatewaysCompilerPass::validateGatewayNames($classes); - - $names = $this->gatewayLocator->getNames(); - GatewaysCompilerPass::compileGatewayNames($names); - - $io->success(self::SUCCESS_MESSAGE); - - return Command::SUCCESS; - } catch (\Exception $e) { - $io->error(self::FAILURE_MESSAGE); - - throw $e; - } - } -} diff --git a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php deleted file mode 100755 index 21acaf7..0000000 --- a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php +++ /dev/null @@ -1,171 +0,0 @@ -isAbstract()) { - continue; - } - - $classes[] = $reflection->getName(); - } - - return $classes; - } - - /** - * Ensures the gateway names are unique for each gateway. - * - * @param array $gatewayClasses Fully-qualified Gateway class names - * - * @throws \Exception If there are two different Gateway classes that return the same name string - */ - public static function validateGatewayNames(array $gatewayClasses): void - { - $gatewaysValidated = []; - foreach ($gatewayClasses as $gatewayClass) { - $gatewayName = $gatewayClass::getName(); - - if (\array_key_exists($gatewayName, $gatewaysValidated)) { - $exceptionMessage = sprintf( - "Duplicate Gateway name '%s' from class %s, name is already in use by class %s", - $gatewayName, - $gatewayClass, - $gatewaysValidated[$gatewayName] - ); - - throw new \Exception($exceptionMessage); - } - - $gatewaysValidated[$gatewayName] = $gatewayClass; - } - } - - /** - * Stores the gateway names in disk. - * - * @param array $names The names returned by the gateways - * - * @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations - */ - public static function compileGatewayNames(array $names): void - { - self::writeLockFile($names); - } - - public function process(ContainerBuilder $container): void - { - $namespace = self::getGatewaysNamespace(); - - $classesDir = join(DIRECTORY_SEPARATOR, [ - $container->getParameter('kernel.project_dir'), - 'src', - ...\array_slice(explode('\\', $namespace), 1), - ]); - - $gatewayClasses = self::getGatewayClasses($classesDir, $namespace); - self::validateGatewayNames($gatewayClasses); - - $gatewayNames = $this->getGatewayNames($gatewayClasses); - self::compileGatewayNames($gatewayNames); - } -} diff --git a/src/Kernel.php b/src/Kernel.php index 6d80b02..4b9668a 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -2,7 +2,6 @@ namespace App; -use App\DependencyInjection\Compiler\GatewaysCompilerPass; use App\DependencyInjection\Compiler\VersionedResourcePass; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -15,11 +14,6 @@ class Kernel extends BaseKernel protected function build(ContainerBuilder $container): void { - $container->addCompilerPass( - new GatewaysCompilerPass(), - PassConfig::TYPE_AFTER_REMOVING - ); - $container->addCompilerPass( new VersionedResourcePass(), PassConfig::TYPE_AFTER_REMOVING From a7a243341b02bb2ac01980228c7f11c25cff50a3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 12:16:19 +0100 Subject: [PATCH 76/86] Remove dependencies on GatewaysCompilerPass --- src/Gateway/GatewayLocator.php | 41 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Gateway/GatewayLocator.php b/src/Gateway/GatewayLocator.php index 06e1898..99eca0d 100644 --- a/src/Gateway/GatewayLocator.php +++ b/src/Gateway/GatewayLocator.php @@ -2,7 +2,6 @@ namespace App\Gateway; -use App\DependencyInjection\Compiler\GatewaysCompilerPass; use App\Entity\Gateway\Checkout; class GatewayLocator @@ -19,7 +18,7 @@ class GatewayLocator $this->gatewaysByClass[$gateway::class] = $gateway; } - GatewaysCompilerPass::validateGatewayNames($this->gatewaysByClass); + self::validateGatewayNames($this->gatewaysByClass); foreach ($this->gatewaysByClass as $class => $gateway) { $this->gatewaysByName[$gateway::getName()] = $gateway; @@ -42,16 +41,6 @@ class GatewayLocator return \array_keys($this->gatewaysByName); } - /** - * @return array List of the available Gateway names - * - * @see \App\DependencyInjection\Compiler\GatewaysCompilerPass::compileGatewayNames() - */ - public static function getNamesStatic(): array - { - return explode(PHP_EOL, \file_get_contents(GatewaysCompilerPass::getLockFile())); - } - /** * @return GatewayInterface[] */ @@ -86,4 +75,32 @@ class GatewayLocator return $this->getGateway($gateway); } + + /** + * Ensures the gateway names are unique for each gateway. + * + * @param array $gatewayClasses Fully-qualified Gateway class names + * + * @throws \Exception If there are two different Gateway classes that return the same name string + */ + private static function validateGatewayNames(array $gatewayClasses): void + { + $gatewaysValidated = []; + foreach ($gatewayClasses as $gatewayClass) { + $gatewayName = $gatewayClass::getName(); + + if (\array_key_exists($gatewayName, $gatewaysValidated)) { + $exceptionMessage = sprintf( + "Duplicate Gateway name '%s' from class %s, name is already in use by class %s", + $gatewayName, + $gatewayClass, + $gatewaysValidated[$gatewayName] + ); + + throw new \Exception($exceptionMessage); + } + + $gatewaysValidated[$gatewayName] = $gatewayClass; + } + } } From 8ee27990bdd3876040a766ca636a5670acbce509 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 13:16:55 +0100 Subject: [PATCH 77/86] Save inverse side of accounting ownership --- src/Entity/Accounting/Accounting.php | 138 +++++++++++++++++++-------- src/Entity/Project.php | 12 ++- src/Entity/Tipjar.php | 12 ++- src/Entity/User.php | 9 +- 4 files changed, 123 insertions(+), 48 deletions(-) diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php index b7f94db..c19ccb7 100644 --- a/src/Entity/Accounting/Accounting.php +++ b/src/Entity/Accounting/Accounting.php @@ -3,6 +3,9 @@ namespace App\Entity\Accounting; use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Project; +use App\Entity\Tipjar; +use App\Entity\User; use App\Repository\Accounting\AccountingRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -28,11 +31,17 @@ class Accounting #[ORM\Column(length: 3)] private ?string $currency = null; - #[ORM\Column] - private ?int $ownerId = null; - #[ORM\Column(length: 255)] - private ?string $ownerClass = null; + private ?string $owner = null; + + #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])] + private ?User $user = null; + + #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])] + private ?Project $project = null; + + #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])] + private ?Tipjar $tipjar = null; public function __construct() { @@ -60,50 +69,95 @@ class Accounting return $this; } - public function getOwnerId(): ?int + public function getOwner(): AccountingOwnerInterface { - return $this->ownerId; - } + $owner = $this->owner; - private function setOwnerId(?int $ownerId): static - { - // ensure ownership does not change - if ( - $this->ownerId !== null - && $this->ownerId !== $ownerId - ) { - throw new \Exception(self::OWNER_CHANGE_NOT_ALLOWED); - } - - $this->ownerId = $ownerId; - - return $this; - } - - public function getOwnerClass(): ?string - { - return $this->ownerClass; - } - - private function setOwnerClass(string $ownerClass): static - { - // ensure ownership does not change - if ( - $this->ownerClass !== null - && $this->ownerClass !== $ownerClass - ) { - throw new \Exception(self::OWNER_CHANGE_NOT_ALLOWED); - } - - $this->ownerClass = $ownerClass; - - return $this; + return $this->$owner; } public function setOwner(AccountingOwnerInterface $owner): static { - $this->setOwnerId($owner->getId()); - $this->setOwnerClass($owner::class); + if ($owner instanceof User) { + $this->owner = 'user'; + return $this->setUser($owner); + } + + if ($owner instanceof Project) { + $this->owner = 'project'; + return $this->setProject($owner); + } + + if ($owner instanceof Tipjar) { + $this->owner = 'tipjar'; + return $this->setTipjar($owner); + } + + throw new \Exception(sprintf('%s is not a recognized AccountingOwnerInterface', $owner::class)); + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + // unset the owning side of the relation if necessary + if ($user === null && $this->user !== null) { + $this->user->setAccounting(null); + } + + // set the owning side of the relation if necessary + if ($user !== null && $user->getAccounting() !== $this) { + $user->setAccounting($this); + } + + $this->user = $user; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + // unset the owning side of the relation if necessary + if ($project === null && $this->project !== null) { + $this->project->setAccounting(null); + } + + // set the owning side of the relation if necessary + if ($project !== null && $project->getAccounting() !== $this) { + $project->setAccounting($this); + } + + $this->project = $project; + + return $this; + } + + public function getTipjar(): ?Tipjar + { + return $this->tipjar; + } + + public function setTipjar(?Tipjar $tipjar): static + { + // unset the owning side of the relation if necessary + if ($tipjar === null && $this->tipjar !== null) { + $this->tipjar->setAccounting(null); + } + + // set the owning side of the relation if necessary + if ($tipjar !== null && $tipjar->getAccounting() !== $this) { + $tipjar->setAccounting($this); + } + + $this->tipjar = $tipjar; return $this; } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index cb8ef41..1609ad3 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -48,7 +48,7 @@ class Project implements AccountingOwnerInterface * A Project's Accounting represents how much money the Project has raised from the community. */ #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(cascade: ['persist'])] + #[ORM\OneToOne(inversedBy: 'project', cascade: ['persist'])] private ?Accounting $accounting = null; /** @@ -67,6 +67,14 @@ class Project implements AccountingOwnerInterface #[ORM\Column(type: 'string', enumType: ProjectStatus::class)] private ProjectStatus $status; + public function __construct() + { + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; + } + public function getId(): ?int { return $this->id; @@ -89,7 +97,7 @@ class Project implements AccountingOwnerInterface return $this->accounting; } - public function setAccounting(Accounting $accounting): static + public function setAccounting(?Accounting $accounting): static { $this->accounting = $accounting; diff --git a/src/Entity/Tipjar.php b/src/Entity/Tipjar.php index 12567bc..621bd84 100644 --- a/src/Entity/Tipjar.php +++ b/src/Entity/Tipjar.php @@ -35,9 +35,17 @@ class Tipjar implements AccountingOwnerInterface private ?string $name = null; #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(cascade: ['persist'])] + #[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist'])] private ?Accounting $accounting = null; + public function __construct() + { + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; + } + public function getId(): ?int { return $this->id; @@ -60,7 +68,7 @@ class Tipjar implements AccountingOwnerInterface return $this->accounting; } - public function setAccounting(Accounting $accounting): static + public function setAccounting(?Accounting $accounting): static { $this->accounting = $accounting; diff --git a/src/Entity/User.php b/src/Entity/User.php index 703ae9a..35d74b8 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -99,7 +99,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs private ?bool $emailConfirmed = null; #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(cascade: ['persist'])] + #[ORM\OneToOne(inversedBy: 'user', cascade: ['persist'])] private ?Accounting $accounting = null; /** @@ -140,6 +140,11 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs public function __construct() { + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; + $this->emailConfirmed = false; $this->active = false; @@ -279,7 +284,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs return $this->accounting; } - public function setAccounting(Accounting $accounting): static + public function setAccounting(?Accounting $accounting): static { $this->accounting = $accounting; From 13102fc1d257fdfa633e6abb0b46fbb03c4ca5a7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 13:17:17 +0100 Subject: [PATCH 78/86] Remove unnecessary listener for accounting ownership inverse side setting --- src/EventListener/AccountingOwnerListener.php | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/EventListener/AccountingOwnerListener.php diff --git a/src/EventListener/AccountingOwnerListener.php b/src/EventListener/AccountingOwnerListener.php deleted file mode 100644 index eeae8a5..0000000 --- a/src/EventListener/AccountingOwnerListener.php +++ /dev/null @@ -1,44 +0,0 @@ -getObject(); - - if (!$entity instanceof AccountingOwnerInterface) { - return; - } - - $accounting = $entity->getAccounting(); - if ($accounting === null || $accounting->getId() === null) { - $accounting = new Accounting(); - $accounting->setOwner($entity); - - $entity->setAccounting($accounting); - - $this->entityManager->persist($entity); - $this->entityManager->flush(); - } - } -} From 31eccc52afe4088101291cac023c107ba1786902 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 13:17:33 +0100 Subject: [PATCH 79/86] Update accounting owner getting --- src/EventListener/WalletTransactionsListener.php | 2 +- src/Mapping/Accounting/AccountingMapper.php | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 73d188d..f868e65 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -31,7 +31,7 @@ final class WalletTransactionsListener ) { $target = $transaction->getTarget(); - if ($target->getOwnerClass() === User::class) { + if ($target->getOwner() instanceof User) { $this->wallet->save($transaction); } } diff --git a/src/Mapping/Accounting/AccountingMapper.php b/src/Mapping/Accounting/AccountingMapper.php index 7c3454f..fac9119 100644 --- a/src/Mapping/Accounting/AccountingMapper.php +++ b/src/Mapping/Accounting/AccountingMapper.php @@ -16,10 +16,7 @@ class AccountingMapper public function toResource(Entity\Accounting $entity): Resource\Accounting { - $owner = $this->entityManager->find( - $entity->getOwnerClass(), - $entity->getOwnerId() - ); + $owner = $entity->getOwner(); $resource = new Resource\Accounting(); $resource->id = $entity->getId(); From b76a1fdc38a847ca4993fb9734b471d5bd193a64 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 13:19:16 +0100 Subject: [PATCH 80/86] Fix tests --- src/Entity/Accounting/Accounting.php | 3 +++ tests/Entity/Accounting/AccountingTest.php | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php index c19ccb7..9bb5665 100644 --- a/src/Entity/Accounting/Accounting.php +++ b/src/Entity/Accounting/Accounting.php @@ -80,16 +80,19 @@ class Accounting { if ($owner instanceof User) { $this->owner = 'user'; + return $this->setUser($owner); } if ($owner instanceof Project) { $this->owner = 'project'; + return $this->setProject($owner); } if ($owner instanceof Tipjar) { $this->owner = 'tipjar'; + return $this->setTipjar($owner); } diff --git a/tests/Entity/Accounting/AccountingTest.php b/tests/Entity/Accounting/AccountingTest.php index 4946e70..6d7f8e3 100644 --- a/tests/Entity/Accounting/AccountingTest.php +++ b/tests/Entity/Accounting/AccountingTest.php @@ -38,8 +38,8 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingA->getId()); - $this->assertEquals($tipjarA->getId(), $accountingA->getOwnerId()); - $this->assertEquals(Tipjar::class, $accountingA->getOwnerClass()); + $this->assertEquals($tipjarA->getId(), $accountingA->getOwner()->getId()); + $this->assertEquals(Tipjar::class, $accountingA->getOwner()::class); $tipjarB = new Tipjar(); $tipjarB->setName('TEST_TIPJAR_B'); @@ -56,7 +56,7 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingB->getId()); - $this->assertEquals($tipjarB->getId(), $accountingB->getOwnerId()); - $this->assertEquals(Tipjar::class, $accountingB->getOwnerClass()); + $this->assertEquals($tipjarB->getId(), $accountingB->getOwner()->getId()); + $this->assertEquals(Tipjar::class, $accountingB->getOwner()::class); } } From 83a9ec70fa208f428a6ae5786c0bd4bce8a2418d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 14:56:55 +0100 Subject: [PATCH 81/86] Improve OPCache config --- docker/php/conf.d/opcache.dev.ini | 3 +-- docker/php/conf.d/opcache.ini | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/php/conf.d/opcache.dev.ini b/docker/php/conf.d/opcache.dev.ini index a173047..6079205 100644 --- a/docker/php/conf.d/opcache.dev.ini +++ b/docker/php/conf.d/opcache.dev.ini @@ -1,8 +1,7 @@ [opcache] opcache.enable=1 opcache.enable_cli=1 -opcache.revalidate_freq=0 -opcache.validate_timestamps=1 +opcache.preload=/app/config/preload.php opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.max_wasted_percentage=10 diff --git a/docker/php/conf.d/opcache.ini b/docker/php/conf.d/opcache.ini index b0d7d47..429582b 100644 --- a/docker/php/conf.d/opcache.ini +++ b/docker/php/conf.d/opcache.ini @@ -1,7 +1,7 @@ [opcache] opcache.enable=1 +opcache.enable_cli=1 opcache.preload=/app/config/preload.php -opcache.preload_user=www-data opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 From 07fae1cb48ddc48d23a8bf5355ebd70681ce5f25 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 15:03:44 +0100 Subject: [PATCH 82/86] Updte StripeGateway dependencies usage --- src/Gateway/Stripe/StripeGateway.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 35a1c84..dc69356 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -12,7 +12,6 @@ use App\Gateway\Link; use App\Gateway\LinkType; use App\Gateway\Tracking; use App\Repository\Gateway\CheckoutRepository; -use App\Repository\UserRepository; use App\Service\Gateway\CheckoutService; use Doctrine\ORM\EntityManagerInterface; use Stripe\Checkout\Session as StripeSession; @@ -22,7 +21,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\RouterInterface; class StripeGateway implements GatewayInterface { @@ -33,10 +31,8 @@ class StripeGateway implements GatewayInterface public function __construct( private string $stripeApiKey, private string $stripeWebhookSecret, - private RouterInterface $router, private CheckoutService $checkoutService, private CheckoutRepository $checkoutRepository, - private UserRepository $userRepository, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, ) { @@ -138,13 +134,12 @@ class StripeGateway implements GatewayInterface private function getStripeCustomer(Checkout $checkout): string { - if ($checkout->getOrigin()->getOwnerClass() !== User::class) { + $owner = $checkout->getOrigin()->getOwner(); + if (!$owner instanceof User) { return ''; } - $user = $this->userRepository->find($checkout->getOrigin()->getOwnerId()); - - return $user->getEmail(); + return $owner->getEmail(); } private function getStripeMode(Checkout $checkout): string @@ -169,7 +164,7 @@ class StripeGateway implements GatewayInterface 'product_data' => \array_filter([ 'name' => $charge->getTitle(), 'statement_descriptor' => $charge->getDescription(), - ], fn ($v) => $v !== null), + ], fn($v) => $v !== null), ]; if ($charge->getType() === ChargeType::Recurring) { From 621b0b96d825919fa6480f71529e0502e771e5cf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 7 Nov 2024 15:15:26 +0100 Subject: [PATCH 83/86] No space after fn for short arrow functions --- .php-cs-fixer.dist.php | 1 + 1 file changed, 1 insertion(+) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 97cb0b0..74082be 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,6 +8,7 @@ return (new PhpCsFixer\Config()) ->setRules([ '@Symfony' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'function_declaration' => ['closure_fn_spacing' => 'none'], 'single_line_empty_body' => true, 'yoda_style' => [ 'equal' => false, From 8630957dd94de85653bf77a7becd476cb87d168c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 11 Nov 2024 10:09:01 +0100 Subject: [PATCH 84/86] Use financement entity to hold money amounts drawed between wallet statements --- src/Entity/WalletFinancement.php | 79 ++++++++++++ src/Entity/WalletStatement.php | 122 +++++++++++------- .../WalletFinancementRepository.php | 43 ++++++ 3 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 src/Entity/WalletFinancement.php create mode 100644 src/Repository/WalletFinancementRepository.php diff --git a/src/Entity/WalletFinancement.php b/src/Entity/WalletFinancement.php new file mode 100644 index 0000000..ea13819 --- /dev/null +++ b/src/Entity/WalletFinancement.php @@ -0,0 +1,79 @@ +id; + } + + public function getMoney(): ?Money + { + return $this->money; + } + + public function setMoney(Money $money): static + { + $this->money = $money; + + return $this; + } + + public function getOrigin(): ?WalletStatement + { + return $this->origin; + } + + public function setOrigin(?WalletStatement $origin): static + { + $this->origin = $origin; + + return $this; + } + + public function getTarget(): ?WalletStatement + { + return $this->target; + } + + public function setTarget(?WalletStatement $target): static + { + $this->target = $target; + + return $this; + } +} diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index 4f109f4..12625bb 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -2,7 +2,7 @@ namespace App\Entity; -use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata as API; use App\Entity\Accounting\Transaction; use App\Gateway\Wallet\StatementDirection; use App\Repository\WalletStatementRepository; @@ -10,7 +10,15 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -#[ApiResource()] +/** + * WalletStatements are used by the Wallet Gateway to track available balances and money movements.\ + * \ + * Statements are relative to a Transaction, which either:\ + * \ + * a. Targets an User's Accounting, for which an incoming Statement will be created and which will hold the money until it is spent.\ + * b. Originates from an User's Accounting via the Wallet Gateway, which will create an outgoing Statement with money financed from previous incoming statements. + */ +#[API\ApiResource()] #[ORM\Entity(repositoryClass: WalletStatementRepository::class)] class WalletStatement { @@ -23,33 +31,44 @@ class WalletStatement #[ORM\JoinColumn(nullable: false)] private ?Transaction $transaction = null; + /** + * An `incoming` direction Statement means the Transaction saved money into the wallet.\ + * An `outgoing` direction Statement means the Transaction spent money from the wallet. + */ #[ORM\Column(enumType: StatementDirection::class)] private ?StatementDirection $direction = null; + /** + * For incoming Statements, the balance tells the available money.\ + * i.e: money not used yet by outgoing Statements. + */ #[ORM\Embedded(class: Money::class)] private ?Money $balance = null; /** - * The Statements from which the money in the Transaction was drawed out of. - * - * @var Collection + * A collection of Financements for outgoing Statements that were made using money from this Statement. + * + * Only not empty for incoming Statements. + * + * @var Collection */ - #[ORM\ManyToMany(targetEntity: WalletStatement::class, mappedBy: 'financesTo')] - private Collection $financedBy; + #[ORM\OneToMany(mappedBy: 'origin', targetEntity: WalletFinancement::class)] + private Collection $financesTo; /** - * The Statements that drawed money out of this Statement for their Transactions.\ - * Inverse of `financedBy`. - * - * @var Collection + * A collection of Financements from incoming Statements that provided money for this Statement. + * + * Only not empty for outgoing Statements. + * + * @var Collection */ - #[ORM\ManyToMany(targetEntity: WalletStatement::class, inversedBy: 'financedBy')] - private Collection $financesTo; + #[ORM\OneToMany(mappedBy: 'target', targetEntity: WalletFinancement::class)] + private Collection $financedBy; public function __construct() { - $this->financedBy = new ArrayCollection(); $this->financesTo = new ArrayCollection(); + $this->financedBy = new ArrayCollection(); } public function getId(): ?int @@ -94,52 +113,61 @@ class WalletStatement } /** - * @return Collection - */ - public function getFinancedBy(): Collection - { - return $this->financedBy; - } - - public function addFinancedBy(WalletStatement $financedBy): static - { - if (!$this->financedBy->contains($financedBy)) { - $this->financedBy->add($financedBy); - $financedBy->addFinanceTo($this); - } - - return $this; - } - - public function removeFinancedBy(WalletStatement $financedBy): static - { - if ($this->financedBy->removeElement($financedBy)) { - $financedBy->removeFinanceTo($this); - } - - return $this; - } - - /** - * @return Collection + * @return Collection */ public function getFinancesTo(): Collection { return $this->financesTo; } - public function addFinanceTo(WalletStatement $finance): static + public function addFinancesTo(WalletFinancement $financesTo): static { - if (!$this->financesTo->contains($finance)) { - $this->financesTo->add($finance); + if (!$this->financesTo->contains($financesTo)) { + $this->financesTo->add($financesTo); + $financesTo->setOrigin($this); } return $this; } - public function removeFinanceTo(WalletStatement $finance): static + public function removeFinancesTo(WalletFinancement $financesTo): static { - $this->financesTo->removeElement($finance); + if ($this->financesTo->removeElement($financesTo)) { + // set the owning side to null (unless already changed) + if ($financesTo->getOrigin() === $this) { + $financesTo->setOrigin(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getFinancedBy(): Collection + { + return $this->financedBy; + } + + public function addFinancedBy(WalletFinancement $financedBy): static + { + if (!$this->financedBy->contains($financedBy)) { + $this->financedBy->add($financedBy); + $financedBy->setTarget($this); + } + + return $this; + } + + public function removeFinancedBy(WalletFinancement $financedBy): static + { + if ($this->financedBy->removeElement($financedBy)) { + // set the owning side to null (unless already changed) + if ($financedBy->getTarget() === $this) { + $financedBy->setTarget(null); + } + } return $this; } diff --git a/src/Repository/WalletFinancementRepository.php b/src/Repository/WalletFinancementRepository.php new file mode 100644 index 0000000..7c2729e --- /dev/null +++ b/src/Repository/WalletFinancementRepository.php @@ -0,0 +1,43 @@ + + */ +class WalletFinancementRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, WalletFinancement::class); + } + +// /** +// * @return WalletFinancement[] Returns an array of WalletFinancement objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('w') +// ->andWhere('w.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('w.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?WalletFinancement +// { +// return $this->createQueryBuilder('w') +// ->andWhere('w.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} From 964989aaeb1b6be9402a1cd5d104b6efad2b0480 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 11 Nov 2024 10:46:56 +0100 Subject: [PATCH 85/86] Fix wallet tests --- src/Entity/WalletStatement.php | 17 ++++--- src/Gateway/Wallet/WalletService.php | 41 ++++++++++------- .../WalletFinancementRepository.php | 46 +++++++++---------- tests/Gateway/Wallet/WalletServiceTest.php | 14 +++--- 4 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index 12625bb..08f0ac9 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -47,22 +47,22 @@ class WalletStatement /** * A collection of Financements for outgoing Statements that were made using money from this Statement. - * + * * Only not empty for incoming Statements. - * + * * @var Collection */ - #[ORM\OneToMany(mappedBy: 'origin', targetEntity: WalletFinancement::class)] + #[ORM\OneToMany(mappedBy: 'origin', targetEntity: WalletFinancement::class, cascade: ['persist'])] private Collection $financesTo; /** * A collection of Financements from incoming Statements that provided money for this Statement. - * + * * Only not empty for outgoing Statements. - * + * * @var Collection */ - #[ORM\OneToMany(mappedBy: 'target', targetEntity: WalletFinancement::class)] + #[ORM\OneToMany(mappedBy: 'target', targetEntity: WalletFinancement::class, cascade: ['persist'])] private Collection $financedBy; public function __construct() @@ -93,6 +93,11 @@ class WalletStatement return $this->direction; } + public function hasDirection(StatementDirection $direction): bool + { + return $this->direction === $direction; + } + public function setDirection(StatementDirection $direction): static { $this->direction = $direction; diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index 30f3518..b64f153 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -5,6 +5,7 @@ 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\Library\Economy\MoneyService; use App\Repository\WalletStatementRepository; @@ -33,16 +34,13 @@ class WalletService $statements = $this->getStatements($accounting); foreach ($statements as $statement) { - $transacted = $this->money->toBrick($statement->getTransaction()->getMoney()); + $balance = $this->money->toBrick($statement->getBalance()); - switch ($statement->getDirection()) { - case StatementDirection::Incoming: - $total = $total->plus($transacted); - break; - case StatementDirection::Outgoing: - $total = $total->minus($transacted); - break; + if (!$statement->hasDirection(StatementDirection::Incoming)) { + continue; } + + $total = $total->plus($balance); } return $this->money->toMoney($total); @@ -86,12 +84,17 @@ class WalletService $outgoing->setTransaction($transaction); $outgoing->setDirection(StatementDirection::Outgoing); - $incomings = $this->getStatements($origin); - foreach ($incomings as $incoming) { + $statements = $this->getStatements($origin); + foreach ($statements as $statement) { if ($spendGoal->amount === 0) { break; } + if (!$statement->hasDirection(StatementDirection::Incoming)) { + continue; + } + + $incoming = $statement; $balance = $incoming->getBalance(); if ($balance->amount === 0) { @@ -104,20 +107,24 @@ class WalletService $balanceSpent = $balance; } - $spendGoal = $this->money->substract($balanceSpent, $spendGoal); - $spentTotal = $this->money->add($balanceSpent, $spentTotal); + $financement = new WalletFinancement(); + $financement->setMoney($balanceSpent); - $balance = $this->money->substract($balanceSpent, $balance); - $incoming->setBalance($balance); + $incoming->setBalance($this->money->substract($balanceSpent, $balance)); + $incoming->addFinancesTo($financement); - $outgoing->setBalance($spentTotal); - $outgoing->addFinancedBy($incoming); + $outgoing->setBalance($this->money->add($balanceSpent, $spentTotal)); + $outgoing->addFinancedBy($financement); $this->entityManager->persist($incoming); $this->entityManager->persist($outgoing); - $this->entityManager->flush(); + + $spendGoal = $this->money->substract($balanceSpent, $spendGoal); + $spentTotal = $this->money->add($balanceSpent, $spentTotal); } + $this->entityManager->flush(); + return $outgoing; } } diff --git a/src/Repository/WalletFinancementRepository.php b/src/Repository/WalletFinancementRepository.php index 7c2729e..0704d8c 100644 --- a/src/Repository/WalletFinancementRepository.php +++ b/src/Repository/WalletFinancementRepository.php @@ -16,28 +16,28 @@ class WalletFinancementRepository extends ServiceEntityRepository parent::__construct($registry, WalletFinancement::class); } -// /** -// * @return WalletFinancement[] Returns an array of WalletFinancement objects -// */ -// public function findByExampleField($value): array -// { -// return $this->createQueryBuilder('w') -// ->andWhere('w.exampleField = :val') -// ->setParameter('val', $value) -// ->orderBy('w.id', 'ASC') -// ->setMaxResults(10) -// ->getQuery() -// ->getResult() -// ; -// } + // /** + // * @return WalletFinancement[] Returns an array of WalletFinancement objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('w') + // ->andWhere('w.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('w.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } -// public function findOneBySomeField($value): ?WalletFinancement -// { -// return $this->createQueryBuilder('w') -// ->andWhere('w.exampleField = :val') -// ->setParameter('val', $value) -// ->getQuery() -// ->getOneOrNullResult() -// ; -// } + // public function findOneBySomeField($value): ?WalletFinancement + // { + // return $this->createQueryBuilder('w') + // ->andWhere('w.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } } diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index 41b798a..0b7f95f 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -191,7 +191,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $incoming = new Transaction(); - $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setMoney(new Money(11, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -199,14 +199,14 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(15, 'EUR')); + $outgoing->setMoney(new Money(12, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); $this->walletService->spend($outgoing); $incoming = new Transaction(); - $incoming->setMoney(new Money(5, 'EUR')); + $incoming->setMoney(new Money(13, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -215,7 +215,7 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(10, $balance->amount); + $this->assertEquals(22, $balance->amount); $this->assertEquals('EUR', $balance->currency); $statements = $this->walletService->getStatements($user); @@ -230,21 +230,21 @@ class WalletServiceTest extends KernelTestCase $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(5, $statement->getBalance()->amount); + $this->assertEquals(9, $statement->getBalance()->amount); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[2]; - $this->assertEquals(15, $statement->getBalance()->amount); + $this->assertEquals(12, $statement->getBalance()->amount); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(2, $statement->getFinancedBy()); $statement = $statements[3]; - $this->assertEquals(5, $statement->getBalance()->amount); + $this->assertEquals(13, $statement->getBalance()->amount); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $this->assertCount(0, $statement->getFinancesTo()); From 8273ee885061304eb4203644083b371c39b8bd28 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 11 Nov 2024 11:16:33 +0100 Subject: [PATCH 86/86] Add assertions to test for financements in wallet service test --- tests/Gateway/Wallet/WalletServiceTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index 0b7f95f..d1cb253 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -169,12 +169,24 @@ class WalletServiceTest extends KernelTestCase $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->assertSame($financement, $statement->getFinancedBy()[0]); + $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); + $statement = $statements[2]; $this->assertEquals(30, $statement->getBalance()->amount); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); $this->assertEquals('EUR', $balance->currency); $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->assertSame($financement, $statement->getFinancedBy()[0]); + $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); } public function testBalanceIsFIFO()