From b5104e743e00c2c63e553923c671054a7460e80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Mon, 3 Jun 2024 16:59:37 +0200 Subject: [PATCH 001/191] add doctrine encrypt bundle --- composer.json | 1 + composer.lock | 421 +++++++++++++++++++++++++++++++++++++++++++++++++- symfony.lock | 12 ++ 3 files changed, 432 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index bf1ddf9..9a1804f 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.16", + "doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4", "nelmio/cors-bundle": "^2.3", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", diff --git a/composer.lock b/composer.lock index f350753..54f3569 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4c6f6e7a3a4506e1cede90bd2a00e33a", + "content-hash": "d3d1fa04683f1bed7130f9f83cd534aa", "packages": [ { "name": "api-platform/core", @@ -394,6 +394,82 @@ ], "time": "2023-09-23T21:17:11+00:00" }, + { + "name": "doctrine/annotations", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", + "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.1" + }, + "time": "2023-02-02T22:02:53+00:00" + }, { "name": "doctrine/cache", "version": "2.2.0", @@ -1716,6 +1792,71 @@ }, "time": "2022-05-23T21:33:49+00:00" }, + { + "name": "doctrineencryptbundle/doctrine-encrypt-bundle", + "version": "5.4.0", + "source": { + "type": "git", + "url": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle.git", + "reference": "aa60745e1548213f11467a1d41308d6c03f23faf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/aa60745e1548213f11467a1d41308d6c03f23faf", + "reference": "aa60745e1548213f11467a1d41308d6c03f23faf", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.13|^2.0", + "doctrine/doctrine-bundle": "^2.0.8|^2.1", + "doctrine/orm": "^2.5", + "paragonie/halite": "^4.6|^5.0", + "php": "^7.2|^8.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "defuse/php-encryption": "^2.1", + "doctrine/cache": "^1.11", + "jetbrains/phpstorm-attributes": "^1.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^7.0" + }, + "suggest": { + "defuse/php-encryption": "Alternative for halite for use with older php-versions", + "ext-sodium": "Required to use halite encryption library.", + "paragonie/sodium_compat": "Alternative for ext-sodium for use with older php-versions" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ambta\\DoctrineEncryptBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Encrypted symfony entity's by verified and standardized libraries", + "keywords": [ + "Defuse", + "decrypt", + "doctrine", + "encrypt", + "halite", + "symfony" + ], + "support": { + "issues": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/issues", + "source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.0" + }, + "time": "2024-03-02T18:14:49+00:00" + }, { "name": "gedmo/doctrine-extensions", "version": "v3.15.0", @@ -2070,6 +2211,282 @@ }, "time": "2023-11-30T16:41:19+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/halite", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/paragonie/halite.git", + "reference": "aee234711b29cccb4a17aaaf6104fc542862fc1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/halite/zipball/aee234711b29cccb4a17aaaf6104fc542862fc1e", + "reference": "aee234711b29cccb4a17aaaf6104fc542862fc1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "paragonie/constant_time_encoding": "^2|^3", + "paragonie/hidden-string": "^1|^2", + "paragonie/sodium_compat": "^1|^2", + "php": "^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\Halite\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "High-level cryptography interface powered by libsodium", + "homepage": "https://github.com/paragonie/halite", + "keywords": [ + "Argon2i", + "BLAKE", + "BLAKE2", + "BLAKE2b", + "Curve25519", + "Ed25519", + "X25519", + "Xsalsa20", + "argon2", + "cryptography", + "encryption", + "ext-sodium", + "hashing", + "libsodium", + "password", + "public-key", + "signatures", + "sodium" + ], + "support": { + "docs": "https://github.com/paragonie/halite/tree/master/doc", + "issues": "https://github.com/paragonie/halite/issues", + "source": "https://github.com/paragonie/halite/tree/v5.1.2" + }, + "time": "2024-05-08T12:59:43+00:00" + }, + { + "name": "paragonie/hidden-string", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/hidden-string.git", + "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7", + "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2|^3", + "php": "^7.4|^8" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\HiddenString\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Encapsulate strings in an object to hide them from stack traces", + "homepage": "https://github.com/paragonie/hidden-string", + "keywords": [ + "hidden", + "stack trace", + "string" + ], + "support": { + "issues": "https://github.com/paragonie/hidden-string/issues", + "source": "https://github.com/paragonie/hidden-string/tree/v2.2.0" + }, + "time": "2024-05-08T12:45:06+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "f65c82939ab17aeff538f9bf6d582f65cc7f255e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/f65c82939ab17aeff538f9bf6d582f65cc7f255e", + "reference": "f65c82939ab17aeff538f9bf6d582f65cc7f255e", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.0.1" + }, + "time": "2024-04-24T12:06:31+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -9476,5 +9893,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/symfony.lock b/symfony.lock index c52da5e..fb787df 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,15 @@ "src/ApiResource/.gitignore" ] }, + "doctrine/annotations": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, "doctrine/doctrine-bundle": { "version": "2.10", "recipe": { @@ -40,6 +49,9 @@ "migrations/.gitignore" ] }, + "doctrineencryptbundle/doctrine-encrypt-bundle": { + "version": "5.4.0" + }, "nelmio/cors-bundle": { "version": "2.3", "recipe": { From 1ba93093f9892ac943e8932854b51692eae071ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Mon, 3 Jun 2024 17:01:34 +0200 Subject: [PATCH 002/191] add user personal entity --- src/Entity/User.php | 20 ++++ src/Entity/UserPersonal.php | 108 ++++++++++++++++++++++ src/Repository/UserPersonalRepository.php | 23 +++++ 3 files changed, 151 insertions(+) create mode 100644 src/Entity/UserPersonal.php create mode 100644 src/Repository/UserPersonalRepository.php diff --git a/src/Entity/User.php b/src/Entity/User.php index 204d048..be8ea75 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -121,6 +121,9 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class)] private Collection $projects; + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?UserPersonal $personalData = null; + public function __construct() { $this->migrated = false; @@ -387,4 +390,21 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs return $this; } + + public function getPersonalData(): ?UserPersonal + { + return $this->personalData; + } + + public function setPersonalData(UserPersonal $personalData): static + { + // set the owning side of the relation if necessary + if ($personalData->getUser() !== $this) { + $personalData->setUser($this); + } + + $this->personalData = $personalData; + + return $this; + } } diff --git a/src/Entity/UserPersonal.php b/src/Entity/UserPersonal.php new file mode 100644 index 0000000..958bc81 --- /dev/null +++ b/src/Entity/UserPersonal.php @@ -0,0 +1,108 @@ +user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getIdentityDocument(): ?string + { + return $this->identity_document; + } + + public function setIdentityDocument(?string $identity_document): static + { + $this->identity_document = $identity_document; + + return $this; + } + + public function getIndetityDocumentType(): ?string + { + return $this->indetity_document_type; + } + + public function setIndetityDocumentType(?string $indetity_document_type): static + { + $this->indetity_document_type = $indetity_document_type; + + return $this; + } + + public function getAddress(): ?string + { + return $this->address; + } + + public function setAddress(?string $address): static + { + $this->address = $address; + + return $this; + } + + public function getPostalCode(): ?string + { + return $this->postal_code; + } + + public function setCP(?string $postal_code): static + { + $this->postal_code = $postal_code; + + return $this; + } + +} diff --git a/src/Repository/UserPersonalRepository.php b/src/Repository/UserPersonalRepository.php new file mode 100644 index 0000000..e4036e7 --- /dev/null +++ b/src/Repository/UserPersonalRepository.php @@ -0,0 +1,23 @@ + + * + * @method UserPersonal|null find($id, $lockMode = null, $lockVersion = null) + * @method UserPersonal|null findOneBy(array $criteria, array $orderBy = null) + * @method UserPersonal[] findAll() + * @method UserPersonal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserPersonalRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserPersonal::class); + } +} From afe29835fe3bac4e0d919bdd60626540c3119697 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 1 Aug 2024 08:48:09 +0200 Subject: [PATCH 003/191] Specify XDebug profiling output dir --- docker/php/conf.d/xdebug.dev.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/php/conf.d/xdebug.dev.ini b/docker/php/conf.d/xdebug.dev.ini index a658c4c..30f48bf 100644 --- a/docker/php/conf.d/xdebug.dev.ini +++ b/docker/php/conf.d/xdebug.dev.ini @@ -4,3 +4,4 @@ xdebug.client_host=host.docker.internal xdebug.client_port=9003 xdebug.discover_client_host=0 xdebug.start_with_request=trigger +xdebug.output_dir=/app From 84329dd596e1cbd0f1a45a6af81a2820ea0cbd64 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 1 Aug 2024 08:51:18 +0200 Subject: [PATCH 004/191] Ignore XDebug profiling output --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 268ee02..67d61de 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ /.php-cs-fixer.php /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### + +###> XDebug ### +/cachegrind* +###< XDebug ### From ac6df932dfb2477261abf67577fe6e6d822a275b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 1 Aug 2024 09:08:30 +0200 Subject: [PATCH 005/191] Remove APC ini config --- docker-compose.yml | 1 - docker/php/conf.d/apc.ini | 7 ------- 2 files changed, 8 deletions(-) delete mode 100644 docker/php/conf.d/apc.ini diff --git a/docker-compose.yml b/docker-compose.yml index 9dd6679..3e8c8a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,6 @@ services: container_name: goteo-v4-php volumes: - .:/app - - ./docker/php/conf.d/apc.ini:/usr/local/etc/php/conf.d/apc.ini:ro - ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro nginx: diff --git a/docker/php/conf.d/apc.ini b/docker/php/conf.d/apc.ini deleted file mode 100644 index 01842b6..0000000 --- a/docker/php/conf.d/apc.ini +++ /dev/null @@ -1,7 +0,0 @@ -[apc] -apc.enabled=1 -apc.enable_cli=1 -apc.shm_size=32M -apc.ttl=0 -apc.gc_ttl=3600 -apc.mmap_file_mask=/tmp/apc.XXXXXX From f6f68067014e3cf9731fcb27b5e76b0c37f4640a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Thu, 1 Aug 2024 16:01:33 +0200 Subject: [PATCH 006/191] fixes based in review in #8 --- .env | 3 + composer.json | 1 + composer.lock | 396 ++++++++++--------- config/bundles.php | 1 + config/packages/doctrine_encrypt_bundle.yaml | 11 + src/Entity/UserPersonal.php | 71 ++-- 6 files changed, 246 insertions(+), 237 deletions(-) create mode 100644 config/packages/doctrine_encrypt_bundle.yaml diff --git a/.env b/.env index 8e0a5b7..f854267 100644 --- a/.env +++ b/.env @@ -36,3 +36,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' STRIPE_API_KEY="" STRIPE_WEBHOOK_SECRET="" + +# doctrine-encrypt-bundle +DOCTRINE_ENCRYPT_KEY="" diff --git a/composer.json b/composer.json index 6956b4e..e0b362f 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^2.19", + "doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4", "nelmio/cors-bundle": "^2.3", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", diff --git a/composer.lock b/composer.lock index eb0718d..a53a819 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": "f466394d7b46dccfdd81e934c576acfa", + "content-hash": "875336006aa26d4c1cdf6d9be100cc8c", "packages": [ { "name": "api-platform/core", - "version": "v3.3.7", + "version": "v3.3.11", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "6daec826cf2a26fa6455a0a137c027c71b126f15" + "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/6daec826cf2a26fa6455a0a137c027c71b126f15", - "reference": "6daec826cf2a26fa6455a0a137c027c71b126f15", + "url": "https://api.github.com/repos/api-platform/core/zipball/ea7d443376dfa1f9a05b53060049e7837d2ec7d4", + "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4", "shasum": "" }, "require": { @@ -190,9 +190,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.3.7" + "source": "https://github.com/api-platform/core/tree/v3.3.11" }, - "time": "2024-06-28T09:35:57+00:00" + "time": "2024-07-20T08:19:33+00:00" }, { "name": "beberlei/doctrineextensions", @@ -3131,16 +3131,16 @@ }, { "name": "stripe/stripe-php", - "version": "v15.0.0", + "version": "v15.4.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "96f0dbe9c2b5365d716f86d92a27feecca2cbaee" + "reference": "13581553ea8725b9d0cfff93d24462c4149abec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/96f0dbe9c2b5365d716f86d92a27feecca2cbaee", - "reference": "96f0dbe9c2b5365d716f86d92a27feecca2cbaee", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/13581553ea8725b9d0cfff93d24462c4149abec9", + "reference": "13581553ea8725b9d0cfff93d24462c4149abec9", "shasum": "" }, "require": { @@ -3184,9 +3184,9 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v15.0.0" + "source": "https://github.com/stripe/stripe-php/tree/v15.4.0" }, - "time": "2024-06-24T23:05:11+00:00" + "time": "2024-07-25T21:00:20+00:00" }, { "name": "subiabre/doctrine-snowflakes", @@ -3303,16 +3303,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "287142df5579ce223c485b3872df3efae8390984" + "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/287142df5579ce223c485b3872df3efae8390984", - "reference": "287142df5579ce223c485b3872df3efae8390984", + "url": "https://api.github.com/repos/symfony/cache/zipball/6702d2d777260e6ff3451fee2d7d78ab5f715cdc", + "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc", "shasum": "" }, "require": { @@ -3379,7 +3379,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.8" + "source": "https://github.com/symfony/cache/tree/v6.4.10" }, "funding": [ { @@ -3395,7 +3395,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-17T06:05:49+00:00" }, { "name": "symfony/cache-contracts", @@ -3624,16 +3624,16 @@ }, { "name": "symfony/console", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", "shasum": "" }, "require": { @@ -3698,7 +3698,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.9" + "source": "https://github.com/symfony/console/tree/v6.4.10" }, "funding": [ { @@ -3714,20 +3714,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e" + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", "shasum": "" }, "require": { @@ -3779,7 +3779,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.9" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10" }, "funding": [ { @@ -3795,7 +3795,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T10:45:28+00:00" + "time": "2024-07-26T07:32:07+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3866,16 +3866,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "64d024fcb3aa613fe163ecae4aaa836dd6d1c5cd" + "reference": "0de9662441bce4670506d0c371cc819a9d0a7607" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/64d024fcb3aa613fe163ecae4aaa836dd6d1c5cd", - "reference": "64d024fcb3aa613fe163ecae4aaa836dd6d1c5cd", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/0de9662441bce4670506d0c371cc819a9d0a7607", + "reference": "0de9662441bce4670506d0c371cc819a9d0a7607", "shasum": "" }, "require": { @@ -3954,7 +3954,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.9" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.10" }, "funding": [ { @@ -3970,20 +3970,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:25:38+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "55aefa0029adff89ecffdb560820e945c7983f06" + "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/55aefa0029adff89ecffdb560820e945c7983f06", - "reference": "55aefa0029adff89ecffdb560820e945c7983f06", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/2ae0c84cc9be0dc1eeb86016970b63c764d8472e", + "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e", "shasum": "" }, "require": { @@ -4028,7 +4028,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.8" + "source": "https://github.com/symfony/dotenv/tree/v6.4.10" }, "funding": [ { @@ -4044,20 +4044,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-09T18:29:35+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec" + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c9b7cc075b3ab484239855622ca05cb0b99c13ec", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/231f1b2ee80f72daa1972f7340297d67439224f0", + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0", "shasum": "" }, "require": { @@ -4103,7 +4103,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.9" + "source": "https://github.com/symfony/error-handler/tree/v6.4.10" }, "funding": [ { @@ -4119,7 +4119,7 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/event-dispatcher", @@ -4409,16 +4409,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" + "reference": "af29198d87112bebdd397bd7735fbd115997824c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", + "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", + "reference": "af29198d87112bebdd397bd7735fbd115997824c", "shasum": "" }, "require": { @@ -4453,7 +4453,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.8" + "source": "https://github.com/symfony/finder/tree/v6.4.10" }, "funding": [ { @@ -4469,7 +4469,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-24T07:06:38+00:00" }, { "name": "symfony/flex", @@ -4538,16 +4538,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "c1d1cb0e508e11639283e1e6f8918eef0fa524bd" + "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c1d1cb0e508e11639283e1e6f8918eef0fa524bd", - "reference": "c1d1cb0e508e11639283e1e6f8918eef0fa524bd", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6cbdb0cc3ddbb63499262cd3036882b08ee2690b", + "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b", "shasum": "" }, "require": { @@ -4666,7 +4666,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.9" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.10" }, "funding": [ { @@ -4682,20 +4682,20 @@ "type": "tidelift" } ], - "time": "2024-06-26T08:32:27+00:00" + "time": "2024-07-26T13:24:20+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947" + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/27de8cc95e11db7a50b027e71caaab9024545947", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/117f1f20a7ade7bcea28b861fb79160a21a1e37b", + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b", "shasum": "" }, "require": { @@ -4743,7 +4743,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.10" }, "funding": [ { @@ -4759,20 +4759,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-26T12:36:27+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005" + "reference": "147e0daf618d7575b5007055340d09aece5cf068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/147e0daf618d7575b5007055340d09aece5cf068", + "reference": "147e0daf618d7575b5007055340d09aece5cf068", "shasum": "" }, "require": { @@ -4857,7 +4857,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.9" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.10" }, "funding": [ { @@ -4873,7 +4873,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T11:48:06+00:00" + "time": "2024-07-26T14:52:04+00:00" }, { "name": "symfony/intl", @@ -5584,16 +5584,16 @@ }, { "name": "symfony/property-info", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "1a0357ed93a6ab09482435a7818defaa85cad69b" + "reference": "edaea9dcc723cb4a0ab6a00f7d6f8c07c0d8ff77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/1a0357ed93a6ab09482435a7818defaa85cad69b", - "reference": "1a0357ed93a6ab09482435a7818defaa85cad69b", + "url": "https://api.github.com/repos/symfony/property-info/zipball/edaea9dcc723cb4a0ab6a00f7d6f8c07c0d8ff77", + "reference": "edaea9dcc723cb4a0ab6a00f7d6f8c07c0d8ff77", "shasum": "" }, "require": { @@ -5647,7 +5647,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.4.9" + "source": "https://github.com/symfony/property-info/tree/v6.4.10" }, "funding": [ { @@ -5663,20 +5663,20 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-07-26T07:32:07+00:00" }, { "name": "symfony/routing", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58" + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", + "url": "https://api.github.com/repos/symfony/routing/zipball/aad19fe10753ba842f0d653a8db819c4b3affa87", + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87", "shasum": "" }, "require": { @@ -5730,7 +5730,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.8" + "source": "https://github.com/symfony/routing/tree/v6.4.10" }, "funding": [ { @@ -5746,7 +5746,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-15T09:26:24+00:00" }, { "name": "symfony/runtime", @@ -5829,16 +5829,16 @@ }, { "name": "symfony/security-bundle", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "adc34df2fe487a13d4410a237202422294c917b2" + "reference": "50007f4f76632741b62fa9604c5f65807f268b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/adc34df2fe487a13d4410a237202422294c917b2", - "reference": "adc34df2fe487a13d4410a237202422294c917b2", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/50007f4f76632741b62fa9604c5f65807f268b72", + "reference": "50007f4f76632741b62fa9604c5f65807f268b72", "shasum": "" }, "require": { @@ -5921,7 +5921,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.9" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.10" }, "funding": [ { @@ -5937,20 +5937,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:06:43+00:00" + "time": "2024-07-17T10:49:44+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "2d58f4c3ff50b1b4eef0a333c2b1e3eef46807f4" + "reference": "432dec55da108c471adcf58c351af01372a52164" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/2d58f4c3ff50b1b4eef0a333c2b1e3eef46807f4", - "reference": "2d58f4c3ff50b1b4eef0a333c2b1e3eef46807f4", + "url": "https://api.github.com/repos/symfony/security-core/zipball/432dec55da108c471adcf58c351af01372a52164", + "reference": "432dec55da108c471adcf58c351af01372a52164", "shasum": "" }, "require": { @@ -6007,7 +6007,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.9" + "source": "https://github.com/symfony/security-core/tree/v6.4.10" }, "funding": [ { @@ -6023,7 +6023,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T07:59:05+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/security-csrf", @@ -6183,16 +6183,16 @@ }, { "name": "symfony/serializer", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce" + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/56ce31d19127e79647ac53387c7555bdcd5730ce", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9a67fcf320561e96f94d62bbe0e169ac534a5718", + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718", "shasum": "" }, "require": { @@ -6261,7 +6261,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.9" + "source": "https://github.com/symfony/serializer/tree/v6.4.10" }, "funding": [ { @@ -6277,7 +6277,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T07:59:05+00:00" + "time": "2024-07-26T13:13:26+00:00" }, { "name": "symfony/service-contracts", @@ -6426,16 +6426,16 @@ }, { "name": "symfony/string", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7" + "reference": "ccf9b30251719567bfd46494138327522b9a9446" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/76792dbd99690a5ebef8050d9206c60c59e681d7", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7", + "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", + "reference": "ccf9b30251719567bfd46494138327522b9a9446", "shasum": "" }, "require": { @@ -6492,7 +6492,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.9" + "source": "https://github.com/symfony/string/tree/v6.4.10" }, "funding": [ { @@ -6508,7 +6508,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:25:38+00:00" + "time": "2024-07-22T10:21:14+00:00" }, { "name": "symfony/translation-contracts", @@ -6783,16 +6783,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01" + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/ee0a4d6a327a963aee094f730da238f7ea18cb01", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01", + "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", "shasum": "" }, "require": { @@ -6860,7 +6860,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.9" + "source": "https://github.com/symfony/validator/tree/v6.4.10" }, "funding": [ { @@ -6876,20 +6876,20 @@ "type": "tidelift" } ], - "time": "2024-06-22T07:42:41+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172" + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c31566e4ca944271cc8d8ac6887cbf31b8c6a172", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", "shasum": "" }, "require": { @@ -6945,7 +6945,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.9" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" }, "funding": [ { @@ -6961,7 +6961,7 @@ "type": "tidelift" } ], - "time": "2024-06-27T13:23:14+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/var-exporter", @@ -7456,30 +7456,38 @@ }, { "name": "composer/pcre", - "version": "3.1.4", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "04229f163664973f68f38f6f73d917799168ef24" + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", - "reference": "04229f163664973f68f38f6f73d917799168ef24", + "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<1.11.8" + }, "require-dev": { - "phpstan/phpstan": "^1.3", + "phpstan/phpstan": "^1.11.8", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { @@ -7507,7 +7515,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.4" + "source": "https://github.com/composer/pcre/tree/3.2.0" }, "funding": [ { @@ -7523,20 +7531,20 @@ "type": "tidelift" } ], - "time": "2024-05-27T13:40:54+00:00" + "time": "2024-07-25T09:36:02+00:00" }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { @@ -7588,7 +7596,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "source": "https://github.com/composer/semver/tree/3.4.2" }, "funding": [ { @@ -7604,7 +7612,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-07-12T11:35:52+00:00" }, { "name": "composer/xdebug-handler", @@ -7845,16 +7853,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.59.3", + "version": "v3.61.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29" + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/30ba9ecc2b0e5205e578fe29973c15653d9bfd29", - "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", "shasum": "" }, "require": { @@ -7936,7 +7944,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.59.3" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" }, "funding": [ { @@ -7944,7 +7952,7 @@ "type": "github" } ], - "time": "2024-06-16T14:17:03+00:00" + "time": "2024-07-31T14:33:15+00:00" }, { "name": "masterminds/html5", @@ -8570,45 +8578,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.19", + "version": "9.6.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + "reference": "49d7820565836236411f5dc002d16dd689cde42f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", + "reference": "49d7820565836236411f5dc002d16dd689cde42f", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -8653,7 +8661,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" }, "funding": [ { @@ -8669,7 +8677,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:35:58+00:00" + "time": "2024-07-10T11:45:39+00:00" }, { "name": "react/cache", @@ -9045,31 +9053,31 @@ }, { "name": "react/socket", - "version": "v1.15.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/dns": "^1.11", + "react/dns": "^1.13", "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.6 || ^1.2.1", - "react/stream": "^1.2" + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4 || ^3 || ^2", + "react/async": "^4.3 || ^3.3 || ^2", "react/promise-stream": "^1.4", - "react/promise-timer": "^1.10" + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { @@ -9113,7 +9121,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.15.0" + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { @@ -9121,7 +9129,7 @@ "type": "open_collective" } ], - "time": "2023-12-15T11:02:10+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { "name": "react/stream", @@ -10366,16 +10374,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045" + "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/6e9db0025db565bcf8f1d46ed734b549e51e6045", - "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045", + "url": "https://api.github.com/repos/symfony/http-client/zipball/b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", + "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", "shasum": "" }, "require": { @@ -10439,7 +10447,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.9" + "source": "https://github.com/symfony/http-client/tree/v6.4.10" }, "funding": [ { @@ -10455,7 +10463,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T07:59:05+00:00" + "time": "2024-07-15T09:26:24+00:00" }, { "name": "symfony/http-client-contracts", @@ -10696,16 +10704,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v7.1.2", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "8eb63f1c0e2001f97b3cd9ed550b18765cdeb1c8" + "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/8eb63f1c0e2001f97b3cd9ed550b18765cdeb1c8", - "reference": "8eb63f1c0e2001f97b3cd9ed550b18765cdeb1c8", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e823122d31935eb711e2767c31f3d71cb0b87fb1", + "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1", "shasum": "" }, "require": { @@ -10758,7 +10766,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.2" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.3" }, "funding": [ { @@ -10774,7 +10782,7 @@ "type": "tidelift" } ], - "time": "2024-06-25T19:55:06+00:00" + "time": "2024-07-26T12:41:01+00:00" }, { "name": "symfony/process", @@ -11004,16 +11012,16 @@ }, { "name": "zenstruck/foundry", - "version": "v1.38.2", + "version": "v1.38.3", "source": { "type": "git", "url": "https://github.com/zenstruck/foundry.git", - "reference": "841bb70df8e6fa5110b85dec1e851a28b7ac496e" + "reference": "112e3a6ce1d75627a3fbfd6cd009055994167edb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/841bb70df8e6fa5110b85dec1e851a28b7ac496e", - "reference": "841bb70df8e6fa5110b85dec1e851a28b7ac496e", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/112e3a6ce1d75627a3fbfd6cd009055994167edb", + "reference": "112e3a6ce1d75627a3fbfd6cd009055994167edb", "shasum": "" }, "require": { @@ -11086,7 +11094,7 @@ ], "support": { "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v1.38.2" + "source": "https://github.com/zenstruck/foundry/tree/v1.38.3" }, "funding": [ { @@ -11094,7 +11102,7 @@ "type": "github" } ], - "time": "2024-06-19T12:38:34+00:00" + "time": "2024-07-04T19:59:49+00:00" } ], "aliases": [], @@ -11108,5 +11116,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index 070b2d5..19b2cda 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -12,4 +12,5 @@ return [ Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine_encrypt_bundle.yaml b/config/packages/doctrine_encrypt_bundle.yaml new file mode 100644 index 0000000..1a17a89 --- /dev/null +++ b/config/packages/doctrine_encrypt_bundle.yaml @@ -0,0 +1,11 @@ +ambta_doctrine_encrypt: + # If you want, you can use your own Encryptor. Encryptor must implements EncryptorInterface interface + # Default: Halite + # encryptor_class: Halite + + # Path where to store the keyfiles + # Default: '%kernel.project_dir%' + # secret_directory_path: '%kernel.project_dir%' + + # Secret string + secret: '%env(CRYPTO_SECRET)%' diff --git a/src/Entity/UserPersonal.php b/src/Entity/UserPersonal.php index 958bc81..a432fff 100644 --- a/src/Entity/UserPersonal.php +++ b/src/Entity/UserPersonal.php @@ -2,10 +2,17 @@ namespace App\Entity; +use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; use ApiPlatform\Metadata\ApiResource; use App\Repository\UserPersonalRepository; use Doctrine\ORM\Mapping as ORM; + +/** + * UserPersonal is the detailed data of a User. + * + * The personal data of a user will be kept encrypted + */ #[ORM\Entity(repositoryClass: UserPersonalRepository::class)] #[ApiResource] class UserPersonal @@ -15,23 +22,26 @@ class UserPersonal #[ORM\JoinColumn(nullable: false)] private ?User $user = null; - #[ORM\Column(length: 255)] - private ?string $name = null; - + /** + * The Identity Document number of the User. + */ #[ORM\Column(length: 255, nullable: true)] #[Encrypted] - private ?string $identity_document = null; - - #[ORM\Column(length: 255, nullable: true)] - private ?string $indetity_document_type = null; + private ?string $identityDocument = null; + /** + * The type of indentity document that the User has, ie: Passport, DNI, NIE, etc. + */ #[ORM\Column(length: 255, nullable: true)] #[Encrypted] - private ?string $address = null; + private ?string $identityDocumentType = null; + /** + * The Postal Code of where the User lives. + */ #[ORM\Column(length: 255, nullable: true)] #[Encrypted] - private ?string $postal_code = null; + private ?string $postalCode = null; public function getUser(): ?User { @@ -45,64 +55,39 @@ class UserPersonal return $this; } - public function getName(): ?string - { - return $this->name; - } - - public function setName(string $name): static - { - $this->name = $name; - - return $this; - } - public function getIdentityDocument(): ?string { - return $this->identity_document; + return $this->identityDocument; } - public function setIdentityDocument(?string $identity_document): static + public function setIdentityDocument(?string $identityDocument): static { - $this->identity_document = $identity_document; + $this->identityDocument = $identityDocument; return $this; } public function getIndetityDocumentType(): ?string { - return $this->indetity_document_type; + return $this->identityDocumentType; } - public function setIndetityDocumentType(?string $indetity_document_type): static + public function setIndetityDocumentType(?string $identityDocumentType): static { - $this->indetity_document_type = $indetity_document_type; - - return $this; - } - - public function getAddress(): ?string - { - return $this->address; - } - - public function setAddress(?string $address): static - { - $this->address = $address; + $this->identityDocumentType = $identityDocumentType; return $this; } public function getPostalCode(): ?string { - return $this->postal_code; + return $this->postalCode; } - public function setCP(?string $postal_code): static + public function setPostalCode(?string $postalCode): static { - $this->postal_code = $postal_code; + $this->postalCode = $postalCode; return $this; } - } From 8296aab0198de7d79d9083b92b39b58117e14546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Thu, 1 Aug 2024 16:02:08 +0200 Subject: [PATCH 007/191] add .Halite.key to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 268ee02..ffaa4f7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ /.php-cs-fixer.php /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### + +# doctrine-encrypt-bundle +.Halite.key From d797c4f1146e136798f95fb2588b3857b031002e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Thu, 1 Aug 2024 16:05:45 +0200 Subject: [PATCH 008/191] fix style --- src/Entity/UserPersonal.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Entity/UserPersonal.php b/src/Entity/UserPersonal.php index a432fff..13e86fc 100644 --- a/src/Entity/UserPersonal.php +++ b/src/Entity/UserPersonal.php @@ -7,7 +7,6 @@ use ApiPlatform\Metadata\ApiResource; use App\Repository\UserPersonalRepository; use Doctrine\ORM\Mapping as ORM; - /** * UserPersonal is the detailed data of a User. * From b8b3c9193e3738d0ccda0e48ff8086eb540476b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Thu, 1 Aug 2024 18:13:12 +0200 Subject: [PATCH 009/191] add CRYPTO_SECRET to .env.test --- .env.test | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.test b/.env.test index 9e7162f..92426e3 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st' SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots +CRYPTO_SECRET="CryptoSecret" From a0819babd201ed2b9d53a015580c7f50ac2f50d5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 10 Sep 2024 13:16:40 +0200 Subject: [PATCH 010/191] Normalize .env headers --- .env | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.env b/.env index f854267..acbcea1 100644 --- a/.env +++ b/.env @@ -34,8 +34,11 @@ DATABASE_URL="mysql://goteo:goteo@mariadb:3306/goteo?serverVersion=10.11.2-Maria CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### +###> doctrineencryptbundle/doctrine-encrypt-bundle ### +DOCTRINE_ENCRYPT_KEY="" +###< doctrineencryptbundle/doctrine-encrypt-bundle ### + +###> App\Library\Economy\Payment\StripeGateway ### STRIPE_API_KEY="" STRIPE_WEBHOOK_SECRET="" - -# doctrine-encrypt-bundle -DOCTRINE_ENCRYPT_KEY="" +###< App\Library\Economy\Payment\StripeGateway ### From 50cf49cd90ed498f8ad8b7ffd013ebee075cbbe4 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 10 Sep 2024 13:19:44 +0200 Subject: [PATCH 011/191] Move symfony/http-client out of require-dev --- composer.json | 2 +- composer.lock | 344 +++++++++++++++++++++++++------------------------- 2 files changed, 173 insertions(+), 173 deletions(-) diff --git a/composer.json b/composer.json index e0b362f..cf4b636 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "symfony/expression-language": "6.4.*", "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", + "symfony/http-client": "6.4.*", "symfony/intl": "6.4.*", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", @@ -93,7 +94,6 @@ "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", - "symfony/http-client": "6.4.*", "symfony/maker-bundle": "^1.51", "symfony/phpunit-bridge": "^7.0", "zenstruck/foundry": "^1.36" diff --git a/composer.lock b/composer.lock index a53a819..65523c0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "875336006aa26d4c1cdf6d9be100cc8c", + "content-hash": "20e503cd4b7c41eca5ee6fcf83fa4ae8", "packages": [ { "name": "api-platform/core", @@ -4684,6 +4684,177 @@ ], "time": "2024-07-26T13:24:20+00:00" }, + { + "name": "symfony/http-client", + "version": "v6.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65", + "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-08-26T06:30:21+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, { "name": "symfony/http-foundation", "version": "v6.4.10", @@ -10372,177 +10543,6 @@ ], "time": "2024-05-31T14:49:08+00:00" }, - { - "name": "symfony/http-client", - "version": "v6.4.10", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.10" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-07-15T09:26:24+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" - }, { "name": "symfony/maker-bundle", "version": "v1.60.0", From 1c92875c1ef21c5a05678899bf505108cb171220 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 10 Sep 2024 14:59:59 +0200 Subject: [PATCH 012/191] Add PayPal env vars --- .env | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env b/.env index acbcea1..2851071 100644 --- a/.env +++ b/.env @@ -42,3 +42,8 @@ DOCTRINE_ENCRYPT_KEY="" STRIPE_API_KEY="" STRIPE_WEBHOOK_SECRET="" ###< App\Library\Economy\Payment\StripeGateway ### + +###> App\Library\Economy\Payment\PaypalGateway ### +PAYPAL_CLIENT_ID="" +PAYPAL_CLIENT_SECRET="" +###< App\Library\Economy\Payment\PaypalGateway ### From 2674c7212179c57f6b48d92bc2d69020c40203a5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 10 Sep 2024 15:00:20 +0200 Subject: [PATCH 013/191] Pass necessary env vars to PayPal Gateway --- config/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/services.yaml b/config/services.yaml index 416b148..baf01e9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -58,5 +58,11 @@ services: - '%env(STRIPE_API_KEY)%' - '%env(STRIPE_WEBHOOK_SECRET)%' + App\Library\Economy\Payment\PaypalGateway: + arguments: + - '%env(APP_ENV)%' + - '%env(PAYPAL_CLIENT_ID)%' + - '%env(PAYPAL_CLIENT_SECRET)%' + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones From 2aa9b02bd95da035cf829b40e9681e8e4475e6d0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 10 Sep 2024 15:00:31 +0200 Subject: [PATCH 014/191] Start PayPal Gateway --- src/Library/Economy/Payment/PaypalGateway.php | 86 ++++++++++++++++++- .../Economy/Payment/PaypalGatewayTest.php | 48 +++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/Library/Economy/Payment/PaypalGatewayTest.php diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 934ecad..7aee68c 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -2,10 +2,94 @@ namespace App\Library\Economy\Payment; -abstract class PaypalGateway implements GatewayInterface +use App\Entity\GatewayCheckout; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\HttpClient\HttpOptions; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class PaypalGateway implements GatewayInterface { + public const PAYPAL_API_ADDRESS_LIVE = 'https://api-m.paypal.com'; + public const PAYPAL_API_ADDRESS_SANDBOX = 'https://api-m.sandbox.paypal.com'; + + private CacheInterface $cache; + + public function __construct( + private string $appEnv, + private string $paypalClientId, + private string $paypalClientSecret, + private HttpClientInterface $httpClient, + ) { + $this->cache = new FilesystemAdapter(); + + $httpOptions = new HttpOptions(); + $httpOptions->setBaseUri( + $appEnv === 'prod' + ? self::PAYPAL_API_ADDRESS_LIVE + : self::PAYPAL_API_ADDRESS_SANDBOX + ); + + $this->setHttpClient($httpClient->withOptions($httpOptions->toArray())); + } + public static function getName(): string { return 'paypal'; } + + public function setHttpClient(HttpClientInterface $httpClient): static + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Calls the PayPal API to generate an OAuth2 token + * @return array The API response body + */ + public function generateAuthToken(): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ + 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], + 'body' => 'grant_type=client_credentials' + ]); + + return $response->toArray(); + } + + /** + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one + * @return array The OAuth2 token data + */ + public function getAuthToken(): array + { + return $this->cache->get($this->getName(), function (ItemInterface $item): array { + $tokenData = $this->generateAuthToken(); + + $item->expiresAfter($tokenData['expires_in']); + + return $tokenData; + }); + } + + public function sendData(GatewayCheckout $checkout): GatewayCheckout + { + return $checkout; + } + + public function handleRedirect(Request $request): RedirectResponse + { + return new RedirectResponse(''); + } + + public function handleWebhook(Request $request): Response + { + return new Response(); + } } diff --git a/tests/Library/Economy/Payment/PaypalGatewayTest.php b/tests/Library/Economy/Payment/PaypalGatewayTest.php new file mode 100644 index 0000000..66e6a52 --- /dev/null +++ b/tests/Library/Economy/Payment/PaypalGatewayTest.php @@ -0,0 +1,48 @@ +paypal = static::getContainer()->get(PaypalGateway::class); + } + + public function testAuthenticates(): void + { + $expectedResponseData = ['token_type' => 'Bearer', 'expires_in' => 0, 'access_token' => '']; + $mockResponse = new MockResponse(json_encode($expectedResponseData), [ + 'http_code' => 201, + 'response_headers' => ['Content-Type: application/json'], + ]); + + $httpClient = new MockHttpClient($mockResponse, 'https://testapi.paypal.com'); + $this->paypal = $this->paypal->setHttpClient($httpClient); + + // Act + $responseData = $this->paypal->generateAuthToken(); + + // Assert + $this->assertSame('POST', $mockResponse->getRequestMethod()); + $this->assertSame('https://testapi.paypal.com/v1/oauth2/token', $mockResponse->getRequestUrl()); + + $this->assertArrayHasKey('token_type', $responseData); + $this->assertSame($expectedResponseData['token_type'], $responseData['token_type']); + + $this->assertArrayHasKey('expires_in', $responseData); + $this->assertIsInt($responseData['expires_in']); + + $this->assertArrayHasKey('access_token', $responseData); + $this->assertIsString($responseData['access_token']); + } +} From 650f0e515ff8bdbb9eab6238ab7d6f08c0defada Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:23:06 +0200 Subject: [PATCH 015/191] Add GatewayCheckoutLink resource --- src/Entity/GatewayCheckoutLink.php | 115 ++++++++++++++++++ src/Entity/GatewayCheckoutLinkType.php | 17 +++ .../GatewayCheckoutLinkRepository.php | 43 +++++++ 3 files changed, 175 insertions(+) create mode 100644 src/Entity/GatewayCheckoutLink.php create mode 100644 src/Entity/GatewayCheckoutLinkType.php create mode 100644 src/Repository/GatewayCheckoutLinkRepository.php diff --git a/src/Entity/GatewayCheckoutLink.php b/src/Entity/GatewayCheckoutLink.php new file mode 100644 index 0000000..5fb236a --- /dev/null +++ b/src/Entity/GatewayCheckoutLink.php @@ -0,0 +1,115 @@ +id; + } + + public function getHref(): ?string + { + return $this->href; + } + + public function setHref(string $href): static + { + $this->href = $href; + + return $this; + } + + public function getRel(): ?string + { + return $this->rel; + } + + public function setRel(string $rel): static + { + $this->rel = $rel; + + return $this; + } + + public function getMethod(): ?string + { + return $this->method; + } + + public function setMethod(string $method): static + { + $this->method = $method; + + return $this; + } + + public function getType(): ?GatewayCheckoutLinkType + { + return $this->type; + } + + public function setType(GatewayCheckoutLinkType $type): static + { + $this->type = $type; + + return $this; + } + + public function getCheckout(): ?GatewayCheckout + { + return $this->checkout; + } + + public function setCheckout(?GatewayCheckout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } +} diff --git a/src/Entity/GatewayCheckoutLinkType.php b/src/Entity/GatewayCheckoutLinkType.php new file mode 100644 index 0000000..7c9c079 --- /dev/null +++ b/src/Entity/GatewayCheckoutLinkType.php @@ -0,0 +1,17 @@ + + */ +class GatewayCheckoutLinkRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GatewayCheckoutLink::class); + } + + // /** + // * @return GatewayCheckoutLink[] Returns an array of GatewayCheckoutLink objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('g.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?GatewayCheckoutLink + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} From 4b084f7bce71388a6c1142814385d20ef352e992 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:23:39 +0200 Subject: [PATCH 016/191] GatewayCheckouts reference gateway checkout links via GatewayCheckoutLink collections --- src/Entity/GatewayCheckout.php | 39 +++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index ceceea7..0eea5c3 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -11,7 +11,6 @@ use App\State\GatewayCheckoutProcessor; use App\Validator\GatewayName; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Component\Validator\Constraints as Assert; @@ -74,8 +73,7 @@ class GatewayCheckout private ?string $gateway = null; /** - * An external identifier provided by the Gateway for the payment.\ - * Required when a GatewayCheckout is completed. + * An external identifier provided by the Gateway for the payment. */ #[API\ApiProperty(writable: false)] #[API\ApiFilter(SearchFilter::class)] @@ -83,11 +81,13 @@ class GatewayCheckout private ?string $gatewayReference = null; /** - * The URL where the checkout with the Gateway is available. + * The URLs provided by the Gateway for this checkout. + * + * @var Collection */ #[API\ApiProperty(writable: false)] - #[ORM\Column(type: Types::TEXT, nullable: true)] - private ?string $checkoutUrl = null; + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutLink::class)] + private Collection $links; /** * GatewayCheckout was migrated from an invest record in Goteo v3 platform. @@ -113,6 +113,7 @@ class GatewayCheckout { $this->charges = new ArrayCollection(); $this->status = GatewayCheckoutStatus::Pending; + $this->links = new ArrayCollection(); } public function getId(): ?int @@ -192,14 +193,32 @@ class GatewayCheckout return $this; } - public function getCheckoutUrl(): ?string + /** + * @return Collection + */ + public function getLinks(): Collection { - return $this->checkoutUrl; + return $this->links; } - public function setCheckoutUrl(string $checkoutUrl): static + public function addLink(GatewayCheckoutLink $link): static { - $this->checkoutUrl = $checkoutUrl; + if (!$this->links->contains($link)) { + $this->links->add($link); + $link->setCheckout($this); + } + + return $this; + } + + public function removeLink(GatewayCheckoutLink $link): static + { + if ($this->links->removeElement($link)) { + // set the owning side to null (unless already changed) + if ($link->getCheckout() === $this) { + $link->setCheckout(null); + } + } return $this; } From 4b33881f152e64fc4ce8dc1bbe8be473c1921db0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:55:55 +0200 Subject: [PATCH 017/191] Remove unnecessary readableLink settings --- src/Entity/Accounting.php | 6 ++---- src/Entity/GatewayCharge.php | 3 ++- src/Entity/GatewayCheckout.php | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index 85cd70e..8a7b2c4 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\Constraints as Assert; * Accountings are the receivers and the issuers of Transactions. */ #[ORM\Entity(repositoryClass: AccountingRepository::class)] -#[API\ApiResource] #[API\Get()] #[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')] class Accounting @@ -48,11 +47,11 @@ class Accounting #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])] private ?Tipjar $tipjar = null; - #[API\ApiProperty(writable: false, readableLink: false)] + #[API\ApiProperty(writable: false)] #[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)] private Collection $transactionsIssued; - #[API\ApiProperty(writable: false, readableLink: false)] + #[API\ApiProperty(writable: false)] #[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)] private Collection $transactionsReceived; @@ -172,7 +171,6 @@ class Accounting /** * @return Collection */ - #[API\ApiProperty(readableLink: false)] public function getTransactions(): Collection { $transactions = [ diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index 5a07536..97b8bbe 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -38,6 +38,7 @@ class GatewayCharge /** * The Accounting receiving the consequent Transaction for this GatewayCharge. */ + #[API\ApiProperty(readableLink: true)] #[Assert\NotBlank()] #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] @@ -46,7 +47,7 @@ class GatewayCharge /** * The AccountingTransaction generated for this charge after the checkout with the Gateway. */ - #[API\ApiProperty(writable: false, readableLink: false)] + #[API\ApiProperty(writable: false)] #[ORM\OneToOne(cascade: ['persist', 'remove'])] private ?AccountingTransaction $transaction = null; diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 0eea5c3..c8946c9 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -50,7 +50,6 @@ class GatewayCheckout */ #[Assert\NotBlank()] #[Assert\Count(min: 1)] - #[API\ApiProperty(readableLink: true, writableLink: true)] #[ORM\ManyToMany(targetEntity: GatewayCharge::class, cascade: ['persist'])] private Collection $charges; From c3d6d4cc3e0ff1b90f78476f7b452d149ad5146e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:56:23 +0200 Subject: [PATCH 018/191] Autowire api platform doctrine processor for gateway checkout processor --- src/State/GatewayCheckoutProcessor.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index c5e64ea..fa5c734 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -7,10 +7,13 @@ 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 { public function __construct( + #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private ProcessorInterface $persistProcessor, private GatewayLocator $gatewayLocator, private EntityManagerInterface $entityManager ) { @@ -21,16 +24,11 @@ class GatewayCheckoutProcessor implements ProcessorInterface */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): GatewayCheckout { - $gateway = $this->gatewayLocator->getGatewayOf($data); + $checkout = $this->persistProcessor->process($data, $operation, $uriVariables, $context); - $this->entityManager->persist($data); - $this->entityManager->flush(); + $gateway = $this->gatewayLocator->getGatewayOf($checkout); + $checkout = $gateway->sendData($checkout); - $checkout = $gateway->sendData($data); - - $this->entityManager->persist($checkout); - $this->entityManager->flush(); - - return $checkout; + return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } From 8630e2191eb298ee52db26027f6bf86f5ee299f3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:58:43 +0200 Subject: [PATCH 019/191] Cascade persist checkout links --- 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 c8946c9..ff2f218 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -85,7 +85,7 @@ class GatewayCheckout * @var Collection */ #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutLink::class)] + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutLink::class, cascade: ['persist'])] private Collection $links; /** From be008c2c5080d28514c5f76bb7c806fb59d64120 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 13:59:03 +0200 Subject: [PATCH 020/191] Use new links in stripe gateway --- src/Library/Economy/Payment/StripeGateway.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 7bf9f81..9f1d0d9 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -7,6 +7,8 @@ use App\Controller\GatewaysController; use App\Entity\AccountingTransaction; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; +use App\Entity\GatewayCheckoutLink; +use App\Entity\GatewayCheckoutLinkType; use App\Entity\GatewayCheckoutStatus; use App\Repository\GatewayCheckoutRepository; use Doctrine\ORM\EntityManagerInterface; @@ -63,7 +65,14 @@ class StripeGateway implements GatewayInterface 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $successUrl), ]); - $checkout->setCheckoutUrl($session->url); + $link = new GatewayCheckoutLink(); + + $link->setHref($session->url); + $link->setRel('approve'); + $link->setMethod(Request::METHOD_GET); + $link->setType(GatewayCheckoutLinkType::Consumer); + + $checkout->addLink($link); $checkout->setGatewayReference($session->id); return $checkout; From fc8704a2dc156d645bbab80bd958c254699cbd7a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 14:36:30 +0200 Subject: [PATCH 021/191] Move gateway response type const strings to interface --- src/Library/Economy/Payment/GatewayInterface.php | 3 +++ src/Library/Economy/Payment/StripeGateway.php | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index 7575e5c..23fa1d2 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -23,6 +23,9 @@ use Symfony\Component\HttpFoundation\Response; */ interface GatewayInterface { + public const RESPONSE_TYPE_SUCCESS = 'success'; + public const RESPONSE_TYPE_FAILURE = 'failure'; + /** * @return string A short, unique, descriptive string for this Gateway */ diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 9f1d0d9..3ea7848 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -23,8 +23,6 @@ use Symfony\Component\Routing\RouterInterface; class StripeGateway implements GatewayInterface { - private const RESPONSE_TYPE_SUCCESS = 'success'; - private StripeClient $stripe; public function __construct( From 8156a181ea5cae19b67905b85e2a1cd145e1fda9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 14:37:44 +0200 Subject: [PATCH 022/191] Generate basic order checkout sessions in Paypal with return urls --- src/Library/Economy/Payment/PaypalGateway.php | 126 +++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 7aee68c..0deaa8d 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -2,27 +2,42 @@ namespace App\Library\Economy\Payment; +use App\Controller\GatewaysController; +use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; +use App\Entity\GatewayCheckoutLink; +use App\Entity\GatewayCheckoutLinkType; +use Brick\Money\Money as BrickMoney; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\HttpClient\HttpOptions; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\RouterInterface; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +/** + * @see https://developer.paypal.com/studio/checkout/standard/integrate + */ class PaypalGateway implements GatewayInterface { public const PAYPAL_API_ADDRESS_LIVE = 'https://api-m.paypal.com'; public const PAYPAL_API_ADDRESS_SANDBOX = 'https://api-m.sandbox.paypal.com'; + /** + * @see https://developer.paypal.com/docs/api/orders/v2/ + */ + private const PAYPAL_ORDER_INTENT = 'CAPTURE'; + private CacheInterface $cache; public function __construct( private string $appEnv, private string $paypalClientId, private string $paypalClientSecret, + private RouterInterface $router, private HttpClientInterface $httpClient, ) { $this->cache = new FilesystemAdapter(); @@ -50,21 +65,23 @@ class PaypalGateway implements GatewayInterface } /** - * Calls the PayPal API to generate an OAuth2 token + * Calls the PayPal API to generate an OAuth2 token. + * * @return array The API response body */ public function generateAuthToken(): array { $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], - 'body' => 'grant_type=client_credentials' + 'body' => 'grant_type=client_credentials', ]); return $response->toArray(); } /** - * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. + * * @return array The OAuth2 token data */ public function getAuthToken(): array @@ -80,6 +97,37 @@ class PaypalGateway implements GatewayInterface public function sendData(GatewayCheckout $checkout): GatewayCheckout { + $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'json' => [ + 'intent' => self::PAYPAL_ORDER_INTENT, + 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), + 'payment_source' => $this->getPaypalPaymentSource() + ], + ]); + + $content = json_decode($response->getContent(), true); + + if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) { + throw new \Exception($content['message']); + } + + $checkout->setGatewayReference($content['id']); + + foreach ($content['links'] as $linkData) { + $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) + ? GatewayCheckoutLinkType::Consumer + : GatewayCheckoutLinkType::Platform; + + $link = new GatewayCheckoutLink(); + $link->setHref($linkData['href']); + $link->setRel($linkData['rel']); + $link->setMethod($linkData['method']); + $link->setType($linkType); + + $checkout->addLink($link); + } + return $checkout; } @@ -92,4 +140,76 @@ class PaypalGateway implements GatewayInterface { return new Response(); } + + private function getPaypalMoney(GatewayCharge $charge): array + { + $brick = BrickMoney::ofMinor( + $charge->getMoney()->amount, + $charge->getMoney()->currency + ); + + return [ + 'value' => $brick->getAmount()->__toString(), + 'currency_code' => $brick->getCurrency()->getCurrencyCode(), + ]; + } + + private function getPaypalReference(GatewayCheckout $checkout, GatewayCharge $charge): string + { + return sprintf('CO%d-CH%d', $checkout->getId(), $charge->getId()); + } + + private function getPaypalPurchaseUnits(GatewayCheckout $checkout): array + { + $units = []; + + foreach ($checkout->getCharges() as $charge) { + $money = $this->getPaypalMoney($charge); + $reference = $this->getPaypalReference($checkout, $charge); + + $units[] = [ + 'reference_id' => $reference, + 'items' => [ + [ + 'name' => $charge::MESSAGE_STATEMENT, + 'description' => $charge::MESSAGE_STATEMENT, + 'quantity' => '1', + 'unit_amount' => [ + ...$money, + ], + ], + ], + 'amount' => [ + ...$money, + 'breakdown' => [ + 'item_total' => [ + ...$money, + ], + ], + ], + ]; + } + + return $units; + } + + private function getPaypalPaymentSource(): array + { + $successUrl = $this->router->generate( + GatewaysController::REDIRECT, + [ + 'type' => self::RESPONSE_TYPE_SUCCESS, + 'gateway' => $this->getName(), + ], + RouterInterface::ABSOLUTE_URL + ); + + return [ + 'paypal' => [ + 'experience_context' => [ + 'return_url' => $successUrl, + ], + ], + ]; + } } From dbc67287a86de858c2374195bd2408a0a2bd8d89 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 11 Sep 2024 14:40:27 +0200 Subject: [PATCH 023/191] Add trailing comma to end of paypal body array keys --- src/Library/Economy/Payment/PaypalGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 0deaa8d..08c29ab 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -102,7 +102,7 @@ class PaypalGateway implements GatewayInterface 'json' => [ 'intent' => self::PAYPAL_ORDER_INTENT, 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), - 'payment_source' => $this->getPaypalPaymentSource() + 'payment_source' => $this->getPaypalPaymentSource(), ], ]); From 0cb64782d68c3b53fdf5cd60e4264ea78509ac29 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:00:29 +0200 Subject: [PATCH 024/191] Make gateway checkout processor more generic --- src/State/GatewayCheckoutProcessor.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index fa5c734..4306cb5 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -3,6 +3,7 @@ namespace App\State; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; use ApiPlatform\State\ProcessorInterface; use App\Entity\GatewayCheckout; use App\Library\Economy\Payment\GatewayLocator; @@ -13,22 +14,25 @@ class GatewayCheckoutProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] - private ProcessorInterface $persistProcessor, + private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, private EntityManagerInterface $entityManager - ) { - } + ) {} /** - * @param GatewayCheckout $data + * @param GatewayCheckout|void $data */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): GatewayCheckout + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - $checkout = $this->persistProcessor->process($data, $operation, $uriVariables, $context); + if (!$operation instanceof Post) { + return; + } + + $checkout = $this->innerProcessor->process($data, $operation, $uriVariables, $context); $gateway = $this->gatewayLocator->getGatewayOf($checkout); $checkout = $gateway->sendData($checkout); - return $this->persistProcessor->process($data, $operation, $uriVariables, $context); + return $this->innerProcessor->process($data, $operation, $uriVariables, $context); } } From badb758b4e0aa6f19416d674bbcbc23b173d6503 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:00:56 +0200 Subject: [PATCH 025/191] Remove unnecessary readableLink attribute --- src/Entity/GatewayCharge.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index 97b8bbe..22ff38d 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -38,7 +38,6 @@ class GatewayCharge /** * The Accounting receiving the consequent Transaction for this GatewayCharge. */ - #[API\ApiProperty(readableLink: true)] #[Assert\NotBlank()] #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] From af789e27b5634324e8c3fb1df0bd56c0bc6e72ff Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:36:22 +0200 Subject: [PATCH 026/191] Complete paypal checkout flow via redirection --- src/Library/Economy/Payment/PaypalGateway.php | 73 ++++++++++++++++++- .../Economy/Payment/PaypalGatewayTest.php | 2 - 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 08c29ab..4bebbc2 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -2,12 +2,17 @@ namespace App\Library\Economy\Payment; +use ApiPlatform\Api\IriConverterInterface; use App\Controller\GatewaysController; +use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; use App\Entity\GatewayCheckoutLink; use App\Entity\GatewayCheckoutLinkType; +use App\Entity\GatewayCheckoutStatus; +use App\Repository\GatewayCheckoutRepository; use Brick\Money\Money as BrickMoney; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\HttpClient\HttpOptions; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -26,6 +31,10 @@ class PaypalGateway implements GatewayInterface public const PAYPAL_API_ADDRESS_LIVE = 'https://api-m.paypal.com'; public const PAYPAL_API_ADDRESS_SANDBOX = 'https://api-m.sandbox.paypal.com'; + /** @see https://developer.paypal.com/docs/api/orders/v2/#orders_get!c=200&path=status&t=response */ + public const PAYPAL_STATUS_APPROVED = "APPROVED"; + public const PAYPAL_STATUS_COMPLETED = "COMPLETED"; + /** * @see https://developer.paypal.com/docs/api/orders/v2/ */ @@ -39,6 +48,9 @@ class PaypalGateway implements GatewayInterface private string $paypalClientSecret, private RouterInterface $router, private HttpClientInterface $httpClient, + private EntityManagerInterface $entityManager, + private IriConverterInterface $iriConverter, + private GatewayCheckoutRepository $checkoutRepository ) { $this->cache = new FilesystemAdapter(); @@ -102,7 +114,7 @@ class PaypalGateway implements GatewayInterface 'json' => [ 'intent' => self::PAYPAL_ORDER_INTENT, 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), - 'payment_source' => $this->getPaypalPaymentSource(), + 'payment_source' => $this->getPaypalPaymentSource($checkout), ], ]); @@ -131,9 +143,66 @@ class PaypalGateway implements GatewayInterface return $checkout; } + private function handleSuccess(GatewayCheckout $checkout): GatewayCheckout + { + $checkout->setStatus(GatewayCheckoutStatus::Charged); + + foreach ($checkout->getCharges() as $charge) { + $transaction = new AccountingTransaction(); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $this->entityManager->persist($transaction); + + $charge->setTransaction($transaction); + + $this->entityManager->persist($charge); + } + + $this->entityManager->flush(); + + return $checkout; + } + public function handleRedirect(Request $request): RedirectResponse { - return new RedirectResponse(''); + $token = $request->query->get('token'); + + if ($request->query->get('type') !== self::RESPONSE_TYPE_SUCCESS) { + throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $token)); + } + + $requestUri = sprintf("/v2/checkout/orders/%s", $token); + $request = $this->httpClient->request(Request::METHOD_GET, $requestUri, [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ]); + + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $token)); + } + + $session = \json_decode($request->getContent(), true); + $checkout = $this->checkoutRepository->findOneBy( + ['gatewayReference' => $token] + ); + + if ($checkout === null) { + throw new \Exception(sprintf("PayPal checkout '%s' exists but no GatewayCheckout with that reference was found.", $token)); + } + + if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); + } + + if (!\in_array($session['status'], [self::PAYPAL_STATUS_APPROVED, self::PAYPAL_STATUS_COMPLETED])) { + throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $token)); + } + + $checkout = $this->handleSuccess($checkout); + + // TO-DO: This should redirect the user to a GUI + return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } public function handleWebhook(Request $request): Response diff --git a/tests/Library/Economy/Payment/PaypalGatewayTest.php b/tests/Library/Economy/Payment/PaypalGatewayTest.php index 66e6a52..441d36c 100644 --- a/tests/Library/Economy/Payment/PaypalGatewayTest.php +++ b/tests/Library/Economy/Payment/PaypalGatewayTest.php @@ -29,10 +29,8 @@ class PaypalGatewayTest extends KernelTestCase $httpClient = new MockHttpClient($mockResponse, 'https://testapi.paypal.com'); $this->paypal = $this->paypal->setHttpClient($httpClient); - // Act $responseData = $this->paypal->generateAuthToken(); - // Assert $this->assertSame('POST', $mockResponse->getRequestMethod()); $this->assertSame('https://testapi.paypal.com/v1/oauth2/token', $mockResponse->getRequestUrl()); From 4786401d69a924fb0e851999eb55f97acde2560f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:36:31 +0200 Subject: [PATCH 027/191] Add missing env var --- .env | 1 + 1 file changed, 1 insertion(+) diff --git a/.env b/.env index 2851071..9664fd1 100644 --- a/.env +++ b/.env @@ -36,6 +36,7 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###> doctrineencryptbundle/doctrine-encrypt-bundle ### DOCTRINE_ENCRYPT_KEY="" +CRYPTO_SECRET="" ###< doctrineencryptbundle/doctrine-encrypt-bundle ### ###> App\Library\Economy\Payment\StripeGateway ### From fb3f26c0cba92ad818e84a8c3f552a12283b5520 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:41:03 +0200 Subject: [PATCH 028/191] Update gateway checkout link types --- src/Entity/GatewayCheckoutLink.php | 4 ++-- src/Entity/GatewayCheckoutLinkType.php | 9 ++++----- src/Library/Economy/Payment/PaypalGateway.php | 4 ++-- src/Library/Economy/Payment/StripeGateway.php | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Entity/GatewayCheckoutLink.php b/src/Entity/GatewayCheckoutLink.php index 5fb236a..9ff892a 100644 --- a/src/Entity/GatewayCheckoutLink.php +++ b/src/Entity/GatewayCheckoutLink.php @@ -37,8 +37,8 @@ class GatewayCheckoutLink /** * The type of the link indicates who is the intended user of a link.\ - * `platform` links are for applications to communicate with the Gateway about this checkout.\ - * `consumer` links are for end-users who must visit this link to complete the checkout. + * `debug` links are for developers and platform maintainers to get useful information about the checkout.\ + * `payment` links are for end-users who must visit this link to complete the checkout. */ #[ORM\Column(enumType: GatewayCheckoutLinkType::class)] private ?GatewayCheckoutLinkType $type = null; diff --git a/src/Entity/GatewayCheckoutLinkType.php b/src/Entity/GatewayCheckoutLinkType.php index 7c9c079..5f00cc0 100644 --- a/src/Entity/GatewayCheckoutLinkType.php +++ b/src/Entity/GatewayCheckoutLinkType.php @@ -5,13 +5,12 @@ namespace App\Entity; enum GatewayCheckoutLinkType: string { /** - * A 'platform' type link indicates a link that is to be consumed by the platform. + * A 'debug' type link indicates a link that is helpful to developers and platform maintainers to get info about the checkout. */ - case Platform = 'platform'; + case Debug = 'debug'; /** - * A 'consumer' type link indicates a link that is to be consumed by the platform's end users, - * i.e: people wanting to checkout. + * A 'payment' type link indicates a link where the checkout is available to an end user for payment. */ - case Consumer = 'consumer'; + case Payment = 'payment'; } diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 4bebbc2..71b254d 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -128,8 +128,8 @@ class PaypalGateway implements GatewayInterface foreach ($content['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) - ? GatewayCheckoutLinkType::Consumer - : GatewayCheckoutLinkType::Platform; + ? GatewayCheckoutLinkType::Payment + : GatewayCheckoutLinkType::Debug; $link = new GatewayCheckoutLink(); $link->setHref($linkData['href']); diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 3ea7848..b1f269c 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -68,7 +68,7 @@ class StripeGateway implements GatewayInterface $link->setHref($session->url); $link->setRel('approve'); $link->setMethod(Request::METHOD_GET); - $link->setType(GatewayCheckoutLinkType::Consumer); + $link->setType(GatewayCheckoutLinkType::Payment); $checkout->addLink($link); $checkout->setGatewayReference($session->id); From e8c881e93c6436700861b5149f3295394220820a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:42:17 +0200 Subject: [PATCH 029/191] Styling fixes --- src/Library/Economy/Payment/PaypalGateway.php | 6 +++--- src/State/GatewayCheckoutProcessor.php | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 71b254d..8959ffa 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -32,8 +32,8 @@ class PaypalGateway implements GatewayInterface public const PAYPAL_API_ADDRESS_SANDBOX = 'https://api-m.sandbox.paypal.com'; /** @see https://developer.paypal.com/docs/api/orders/v2/#orders_get!c=200&path=status&t=response */ - public const PAYPAL_STATUS_APPROVED = "APPROVED"; - public const PAYPAL_STATUS_COMPLETED = "COMPLETED"; + public const PAYPAL_STATUS_APPROVED = 'APPROVED'; + public const PAYPAL_STATUS_COMPLETED = 'COMPLETED'; /** * @see https://developer.paypal.com/docs/api/orders/v2/ @@ -173,7 +173,7 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $token)); } - $requestUri = sprintf("/v2/checkout/orders/%s", $token); + $requestUri = sprintf('/v2/checkout/orders/%s', $token); $request = $this->httpClient->request(Request::METHOD_GET, $requestUri, [ 'auth_bearer' => $this->getAuthToken()['access_token'], ]); diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index 4306cb5..5b865ed 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -17,7 +17,8 @@ class GatewayCheckoutProcessor implements ProcessorInterface private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, private EntityManagerInterface $entityManager - ) {} + ) { + } /** * @param GatewayCheckout|void $data From 2f1f79be3876ea6bdcc62f2260e2a2bd8817195c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:47:39 +0200 Subject: [PATCH 030/191] Bump minor versions --- composer.lock | 869 ++++++++++++++++++++++++++------------------------ 1 file changed, 454 insertions(+), 415 deletions(-) diff --git a/composer.lock b/composer.lock index 65523c0..54545f8 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "api-platform/core", - "version": "v3.3.11", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4" + "reference": "05f7d795252d32424b49336e5184557569809796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/ea7d443376dfa1f9a05b53060049e7837d2ec7d4", - "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4", + "url": "https://api.github.com/repos/api-platform/core/zipball/05f7d795252d32424b49336e5184557569809796", + "reference": "05f7d795252d32424b49336e5184557569809796", "shasum": "" }, "require": { @@ -26,13 +26,13 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.4 || ^7.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.0", + "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.0" }, "conflict": { @@ -44,15 +44,56 @@ "elasticsearch/elasticsearch": ">=8.0,<8.4", "phpspec/prophecy": "<1.15", "phpunit/phpunit": "<9.5", - "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/framework-bundle": "6.4.6 || 7.1.6", "symfony/var-exporter": "<6.1.1" }, + "replace": { + "api-platform/doctrine-common": "self.version", + "api-platform/doctrine-odm": "self.version", + "api-platform/doctrine-orm": "self.version", + "api-platform/documentation": "self.version", + "api-platform/elasticsearch": "self.version", + "api-platform/graphql": "self.version", + "api-platform/http-cache": "self.version", + "api-platform/hydra": "self.version", + "api-platform/json-api": "self.version", + "api-platform/json-hal": "self.version", + "api-platform/json-schema": "self.version", + "api-platform/jsonld": "self.version", + "api-platform/laravel": "self.version", + "api-platform/metadata": "self.version", + "api-platform/openapi": "self.version", + "api-platform/parameter-validator": "self.version", + "api-platform/ramsey-uuid": "self.version", + "api-platform/serializer": "self.version", + "api-platform/state": "self.version", + "api-platform/symfony": "self.version", + "api-platform/validator": "self.version" + }, "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", + "api-platform/http-cache": "^3.4 || ^4.0", + "api-platform/hydra": "^3.4 || ^4.0", + "api-platform/json-api": "^3.3 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/jsonld": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/parameter-validator": "^3.4", + "api-platform/ramsey-uuid": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "api-platform/validator": "^3.4 || ^4.0", "behat/behat": "^3.11", "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", - "doctrine/dbal": "^3.4.0", + "doctrine/dbal": "^3.4.0 || ^4.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", "doctrine/mongodb-odm": "^2.2", "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", @@ -61,7 +102,7 @@ "friends-of-behat/mink-browserkit-driver": "^1.3.1", "friends-of-behat/mink-extension": "^2.2", "friends-of-behat/symfony-extension": "^2.1", - "guzzlehttp/guzzle": "^6.0 || ^7.0", + "guzzlehttp/guzzle": "^6.0 || ^7.1", "jangregor/phpstan-prophecy": "^1.0", "justinrainbow/json-schema": "^5.2.1", "phpspec/prophecy-phpunit": "^2.0", @@ -74,41 +115,42 @@ "phpunit/phpunit": "^9.6", "psr/log": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^3.9.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4 || ^2.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", "sebastian/comparator": "<5.0", "soyuka/contexts": "v3.3.9", - "soyuka/pmu": "^0.0.2", + "soyuka/pmu": "^0.0.12", "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.4 || ^7.0", - "symfony/browser-kit": "^6.4 || ^7.0", - "symfony/cache": "^6.4 || ^7.0", - "symfony/config": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/css-selector": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0.12", - "symfony/doctrine-bridge": "^6.4 || ^7.0", - "symfony/dom-crawler": "^6.4 || ^7.0", - "symfony/error-handler": "^6.4 || ^7.0", - "symfony/event-dispatcher": "^6.4 || ^7.0", - "symfony/expression-language": "^6.4 || ^7.0", - "symfony/finder": "^6.4 || ^7.0", - "symfony/form": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/intl": "^6.4 || ^7.0", + "symfony/asset": "^6.4 || ^7.1", + "symfony/browser-kit": "^6.4 || ^7.1", + "symfony/cache": "^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.1", + "symfony/console": "^6.4 || ^7.1", + "symfony/css-selector": "^6.4 || ^7.1", + "symfony/dependency-injection": "^6.4 || ^7.1", + "symfony/doctrine-bridge": "^6.4 || ^7.1", + "symfony/dom-crawler": "^6.4 || ^7.1", + "symfony/error-handler": "^6.4 || ^7.1", + "symfony/event-dispatcher": "^6.4 || ^7.1", + "symfony/expression-language": "^6.4 || ^7.1", + "symfony/finder": "^6.4 || ^7.1", + "symfony/form": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", + "symfony/http-client": "^6.4 || ^7.1", + "symfony/intl": "^6.4 || ^7.1", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4.1 || ^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/security-bundle": "^6.4 || ^7.0", - "symfony/security-core": "^6.4 || ^7.0", - "symfony/stopwatch": "^6.4 || ^7.0", - "symfony/twig-bundle": "^6.4 || ^7.0", - "symfony/uid": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "symfony/web-profiler-bundle": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.1", + "symfony/phpunit-bridge": "^6.4.1 || ^7.1", + "symfony/routing": "^6.4 || ^7.1", + "symfony/security-bundle": "^6.4 || ^7.1", + "symfony/security-core": "^6.4 || ^7.1", + "symfony/stopwatch": "^6.4 || ^7.1", + "symfony/string": "^6.4 || ^7.1", + "symfony/twig-bundle": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.1", + "symfony/yaml": "^6.4 || ^7.1", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "webonyx/graphql-php": "^14.0 || ^15.0" }, @@ -133,31 +175,22 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.1" }, - "projects": [ - "api-platform/doctrine-common", - "api-platform/doctrine-orm", - "api-platform/doctrine-odm", - "api-platform/metadata", - "api-platform/json-schema", - "api-platform/elasticsearch", - "api-platform/jsonld", - "api-platform/hydra", - "api-platform/openapi", - "api-platform/graphql", - "api-platform/http-cache", - "api-platform/documentation", - "api-platform/parameter-validator", - "api-platform/ramsey-uuid", - "api-platform/serializer", - "api-platform/state", - "api-platform/symfony", - "api-platform/validator" - ] + "pmu": { + "projects": [ + "./src/*/composer.json", + "src/Doctrine/*/composer.json" + ] + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } }, "autoload": { "psr-4": { @@ -190,9 +223,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.3.11" + "source": "https://github.com/api-platform/core/tree/v3.4.1" }, - "time": "2024-07-20T08:19:33+00:00" + "time": "2024-09-23T07:27:56+00:00" }, { "name": "beberlei/doctrineextensions", @@ -420,16 +453,16 @@ }, { "name": "doctrine/annotations", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", "shasum": "" }, "require": { @@ -441,10 +474,10 @@ "require-dev": { "doctrine/cache": "^2.0", "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.8.0", + "phpstan/phpstan": "^1.10.28", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6", - "vimeo/psalm": "^4.10" + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" }, "suggest": { "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" @@ -490,9 +523,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.1" + "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, - "time": "2023-02-02T22:02:53+00:00" + "time": "2024-09-05T10:17:24+00:00" }, { "name": "doctrine/cache", @@ -766,16 +799,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.6", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", "shasum": "" }, "require": { @@ -791,12 +824,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.11.5", + "phpstan/phpstan": "1.12.0", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "9.6.19", + "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.1", + "squizlabs/php_codesniffer": "3.10.2", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -859,7 +892,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.6" + "source": "https://github.com/doctrine/dbal/tree/3.9.1" }, "funding": [ { @@ -875,7 +908,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T10:38:17+00:00" + "time": "2024-09-01T13:49:23+00:00" }, { "name": "doctrine/deprecations", @@ -926,16 +959,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "5418e811a14724068e95e0ba43353b903ada530f" + "reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f", - "reference": "5418e811a14724068e95e0ba43353b903ada530f", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca59d84b8e63143ce1aed90cdb333ba329d71563", + "reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563", "shasum": "" }, "require": { @@ -1026,7 +1059,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.0" }, "funding": [ { @@ -1042,7 +1075,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T07:20:37+00:00" + "time": "2024-09-01T09:46:40+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1467,16 +1500,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.0", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "535a70dcbd88b8c6ba945be050977457f4f4c06c" + "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/535a70dcbd88b8c6ba945be050977457f4f4c06c", - "reference": "535a70dcbd88b8c6ba945be050977457f4f4c06c", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/7760fbd0b7cb58bfb50415505a7bab821adf0877", + "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877", "shasum": "" }, "require": { @@ -1499,6 +1532,7 @@ "doctrine/persistence": "^2 || ^3", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", + "fig/log-test": "^1", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.3", @@ -1549,7 +1583,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.0" + "source": "https://github.com/doctrine/migrations/tree/3.8.1" }, "funding": [ { @@ -1565,20 +1599,20 @@ "type": "tidelift" } ], - "time": "2024-06-26T14:12:46+00:00" + "time": "2024-08-28T13:17:28+00:00" }, { "name": "doctrine/orm", - "version": "2.19.6", + "version": "2.19.7", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073" + "reference": "168ac31084226f94d42e7461a40ff5607a56bd35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073", - "reference": "c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073", + "url": "https://api.github.com/repos/doctrine/orm/zipball/168ac31084226f94d42e7461a40ff5607a56bd35", + "reference": "168ac31084226f94d42e7461a40ff5607a56bd35", "shasum": "" }, "require": { @@ -1664,9 +1698,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.6" + "source": "https://github.com/doctrine/orm/tree/2.19.7" }, - "time": "2024-06-26T17:24:40+00:00" + "time": "2024-08-23T06:54:57+00:00" }, { "name": "doctrine/persistence", @@ -1767,16 +1801,16 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" + "reference": "7f83911cc5eba870de7ebb11283972483f7e2891" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/7f83911cc5eba870de7ebb11283972483f7e2891", + "reference": "7f83911cc5eba870de7ebb11283972483f7e2891", "shasum": "" }, "require": { @@ -1816,9 +1850,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" + "source": "https://github.com/doctrine/sql-formatter/tree/1.4.1" }, - "time": "2024-05-08T08:12:09+00:00" + "time": "2024-08-05T20:32:22+00:00" }, { "name": "doctrineencryptbundle/doctrine-encrypt-bundle", @@ -2437,16 +2471,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v2.0.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "f65c82939ab17aeff538f9bf6d582f65cc7f255e" + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/f65c82939ab17aeff538f9bf6d582f65cc7f255e", - "reference": "f65c82939ab17aeff538f9bf6d582f65cc7f255e", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", "shasum": "" }, "require": { @@ -2461,6 +2495,11 @@ "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "files": [ "autoload.php" @@ -2517,9 +2556,9 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v2.0.1" + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" }, - "time": "2024-04-24T12:06:31+00:00" + "time": "2024-09-04T12:51:01+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2698,16 +2737,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.31.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "249f15fb843bf240cf058372dad29e100cee6c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17", + "reference": "249f15fb843bf240cf058372dad29e100cee6c17", "shasum": "" }, "require": { @@ -2739,9 +2778,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.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-09-22T11:32:18+00:00" }, { "name": "psr/cache", @@ -3001,16 +3040,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -3045,9 +3084,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "stof/doctrine-extensions-bundle", @@ -3131,16 +3170,16 @@ }, { "name": "stripe/stripe-php", - "version": "v15.4.0", + "version": "v15.10.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "13581553ea8725b9d0cfff93d24462c4149abec9" + "reference": "3df1a19a33477af9ead8984dbd84e8f637c36199" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/13581553ea8725b9d0cfff93d24462c4149abec9", - "reference": "13581553ea8725b9d0cfff93d24462c4149abec9", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/3df1a19a33477af9ead8984dbd84e8f637c36199", + "reference": "3df1a19a33477af9ead8984dbd84e8f637c36199", "shasum": "" }, "require": { @@ -3184,9 +3223,9 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v15.4.0" + "source": "https://github.com/stripe/stripe-php/tree/v15.10.0" }, - "time": "2024-07-25T21:00:20+00:00" + "time": "2024-09-18T18:38:30+00:00" }, { "name": "subiabre/doctrine-snowflakes", @@ -3303,16 +3342,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc" + "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/6702d2d777260e6ff3451fee2d7d78ab5f715cdc", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc", + "url": "https://api.github.com/repos/symfony/cache/zipball/a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", + "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", "shasum": "" }, "require": { @@ -3379,7 +3418,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.10" + "source": "https://github.com/symfony/cache/tree/v6.4.12" }, "funding": [ { @@ -3395,7 +3434,7 @@ "type": "tidelift" } ], - "time": "2024-07-17T06:05:49+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/cache-contracts", @@ -3624,16 +3663,16 @@ }, { "name": "symfony/console", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", + "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { @@ -3698,7 +3737,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.10" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -3714,20 +3753,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb" + "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", - "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e", + "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e", "shasum": "" }, "require": { @@ -3779,7 +3818,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.12" }, "funding": [ { @@ -3795,7 +3834,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T07:32:07+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3866,16 +3905,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "0de9662441bce4670506d0c371cc819a9d0a7607" + "reference": "b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/0de9662441bce4670506d0c371cc819a9d0a7607", - "reference": "0de9662441bce4670506d0c371cc819a9d0a7607", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af", + "reference": "b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af", "shasum": "" }, "require": { @@ -3954,7 +3993,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.10" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.12" }, "funding": [ { @@ -3970,20 +4009,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-08T12:31:10+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e" + "reference": "815284236cab7d8e1280f53bf562c07a4dfe5954" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/2ae0c84cc9be0dc1eeb86016970b63c764d8472e", - "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/815284236cab7d8e1280f53bf562c07a4dfe5954", + "reference": "815284236cab7d8e1280f53bf562c07a4dfe5954", "shasum": "" }, "require": { @@ -4028,7 +4067,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.10" + "source": "https://github.com/symfony/dotenv/tree/v6.4.12" }, "funding": [ { @@ -4044,7 +4083,7 @@ "type": "tidelift" } ], - "time": "2024-07-09T18:29:35+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/error-handler", @@ -4279,16 +4318,16 @@ }, { "name": "symfony/expression-language", - "version": "v6.4.8", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a" + "reference": "564e109c40d3637053c942a29a58e9434592a8bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", - "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/564e109c40d3637053c942a29a58e9434592a8bf", + "reference": "564e109c40d3637053c942a29a58e9434592a8bf", "shasum": "" }, "require": { @@ -4323,7 +4362,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.8" + "source": "https://github.com/symfony/expression-language/tree/v6.4.11" }, "funding": [ { @@ -4339,20 +4378,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-08-12T09:55:28+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/f810e3cbdf7fdc35983968523d09f349fa9ada12", + "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12", "shasum": "" }, "require": { @@ -4389,7 +4428,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + "source": "https://github.com/symfony/filesystem/tree/v6.4.12" }, "funding": [ { @@ -4405,20 +4444,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/finder", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "af29198d87112bebdd397bd7735fbd115997824c" + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", - "reference": "af29198d87112bebdd397bd7735fbd115997824c", + "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453", "shasum": "" }, "require": { @@ -4453,7 +4492,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.10" + "source": "https://github.com/symfony/finder/tree/v6.4.11" }, "funding": [ { @@ -4469,20 +4508,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:06:38+00:00" + "time": "2024-08-13T14:27:37+00:00" }, { "name": "symfony/flex", - "version": "v2.4.5", + "version": "v2.4.6", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "b0a405f40614c9f584b489d54f91091817b0e26e" + "reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/b0a405f40614c9f584b489d54f91091817b0e26e", - "reference": "b0a405f40614c9f584b489d54f91091817b0e26e", + "url": "https://api.github.com/repos/symfony/flex/zipball/4dc11919791f81d087a12db2ab4c7e044431ef6b", + "reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b", "shasum": "" }, "require": { @@ -4518,7 +4557,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.5" + "source": "https://github.com/symfony/flex/tree/v2.4.6" }, "funding": [ { @@ -4534,20 +4573,20 @@ "type": "tidelift" } ], - "time": "2024-03-02T08:16:47+00:00" + "time": "2024-04-27T10:22:22+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b" + "reference": "6a9665bd1fae37b198429775c6132f193339434f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6cbdb0cc3ddbb63499262cd3036882b08ee2690b", - "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a9665bd1fae37b198429775c6132f193339434f", + "reference": "6a9665bd1fae37b198429775c6132f193339434f", "shasum": "" }, "require": { @@ -4556,7 +4595,7 @@ "php": ">=8.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.12|^7.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.1|^7.0", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", @@ -4666,7 +4705,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.10" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.12" }, "funding": [ { @@ -4682,20 +4721,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T13:24:20+00:00" + "time": "2024-09-20T13:34:56+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65" + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", "shasum": "" }, "require": { @@ -4759,7 +4798,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.11" + "source": "https://github.com/symfony/http-client/tree/v6.4.12" }, "funding": [ { @@ -4775,7 +4814,7 @@ "type": "tidelift" } ], - "time": "2024-08-26T06:30:21+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/http-client-contracts", @@ -4857,16 +4896,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b" + "reference": "133ac043875f59c26c55e79cf074562127cce4d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/117f1f20a7ade7bcea28b861fb79160a21a1e37b", - "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/133ac043875f59c26c55e79cf074562127cce4d2", + "reference": "133ac043875f59c26c55e79cf074562127cce4d2", "shasum": "" }, "require": { @@ -4914,7 +4953,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.10" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.12" }, "funding": [ { @@ -4930,20 +4969,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:36:27+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "147e0daf618d7575b5007055340d09aece5cf068" + "reference": "96df83d51b5f78804f70c093b97310794fd6257b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/147e0daf618d7575b5007055340d09aece5cf068", - "reference": "147e0daf618d7575b5007055340d09aece5cf068", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96df83d51b5f78804f70c093b97310794fd6257b", + "reference": "96df83d51b5f78804f70c093b97310794fd6257b", "shasum": "" }, "require": { @@ -5028,7 +5067,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.10" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.12" }, "funding": [ { @@ -5044,20 +5083,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T14:52:04+00:00" + "time": "2024-09-21T06:02:57+00:00" }, { "name": "symfony/intl", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb" + "reference": "5f64d7218f4078492ca59da94747d7474a2a52c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/50265cdcf5a44bec3fcf487b5d0015aece91d1eb", - "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb", + "url": "https://api.github.com/repos/symfony/intl/zipball/5f64d7218f4078492ca59da94747d7474a2a52c4", + "reference": "5f64d7218f4078492ca59da94747d7474a2a52c4", "shasum": "" }, "require": { @@ -5111,7 +5150,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.8" + "source": "https://github.com/symfony/intl/tree/v6.4.12" }, "funding": [ { @@ -5127,7 +5166,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-20T08:16:53+00:00" }, { "name": "symfony/monolog-bridge", @@ -5363,20 +5402,20 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -5421,7 +5460,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -5437,24 +5476,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -5502,7 +5541,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -5518,24 +5557,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -5582,7 +5621,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -5598,24 +5637,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -5658,7 +5697,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -5674,20 +5713,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:35:24+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/property-access", - "version": "v6.4.8", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a" + "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/e4d9b00983612f9c0013ca37c61affdba2dd975a", - "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a", + "url": "https://api.github.com/repos/symfony/property-access/zipball/866f6cd84f2094cbc6f66ce9752faf749916e2a9", + "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9", "shasum": "" }, "require": { @@ -5735,7 +5774,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.8" + "source": "https://github.com/symfony/property-access/tree/v6.4.11" }, "funding": [ { @@ -5751,7 +5790,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-08-30T16:10:11+00:00" }, { "name": "symfony/property-info", @@ -5838,16 +5877,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87" + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/aad19fe10753ba842f0d653a8db819c4b3affa87", - "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87", + "url": "https://api.github.com/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", "shasum": "" }, "require": { @@ -5901,7 +5940,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.10" + "source": "https://github.com/symfony/routing/tree/v6.4.12" }, "funding": [ { @@ -5917,20 +5956,20 @@ "type": "tidelift" } ], - "time": "2024-07-15T09:26:24+00:00" + "time": "2024-09-20T08:32:26+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0" + "reference": "bfe32a1adf41da4dd7f6b939a039779d7af5497f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", - "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", + "url": "https://api.github.com/repos/symfony/runtime/zipball/bfe32a1adf41da4dd7f6b939a039779d7af5497f", + "reference": "bfe32a1adf41da4dd7f6b939a039779d7af5497f", "shasum": "" }, "require": { @@ -5980,7 +6019,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.8" + "source": "https://github.com/symfony/runtime/tree/v6.4.12" }, "funding": [ { @@ -5996,20 +6035,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-19T13:29:10+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "50007f4f76632741b62fa9604c5f65807f268b72" + "reference": "620be16fceded671823ce6332d06f44bb327096d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/50007f4f76632741b62fa9604c5f65807f268b72", - "reference": "50007f4f76632741b62fa9604c5f65807f268b72", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/620be16fceded671823ce6332d06f44bb327096d", + "reference": "620be16fceded671823ce6332d06f44bb327096d", "shasum": "" }, "require": { @@ -6018,7 +6057,7 @@ "php": ">=8.1", "symfony/clock": "^6.3|^7.0", "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.2|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "symfony/http-foundation": "^6.2|^7.0", @@ -6092,7 +6131,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.10" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.11" }, "funding": [ { @@ -6108,20 +6147,20 @@ "type": "tidelift" } ], - "time": "2024-07-17T10:49:44+00:00" + "time": "2024-08-20T11:22:16+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "432dec55da108c471adcf58c351af01372a52164" + "reference": "8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/432dec55da108c471adcf58c351af01372a52164", - "reference": "432dec55da108c471adcf58c351af01372a52164", + "url": "https://api.github.com/repos/symfony/security-core/zipball/8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f", + "reference": "8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f", "shasum": "" }, "require": { @@ -6178,7 +6217,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.10" + "source": "https://github.com/symfony/security-core/tree/v6.4.12" }, "funding": [ { @@ -6194,7 +6233,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/security-csrf", @@ -6266,16 +6305,16 @@ }, { "name": "symfony/security-http", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "8e70f39626ada36c5492c3aff9369c85d2840948" + "reference": "f6df97af71943cda726dc852335204eac02a716b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/8e70f39626ada36c5492c3aff9369c85d2840948", - "reference": "8e70f39626ada36c5492c3aff9369c85d2840948", + "url": "https://api.github.com/repos/symfony/security-http/zipball/f6df97af71943cda726dc852335204eac02a716b", + "reference": "f6df97af71943cda726dc852335204eac02a716b", "shasum": "" }, "require": { @@ -6334,7 +6373,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.9" + "source": "https://github.com/symfony/security-http/tree/v6.4.12" }, "funding": [ { @@ -6350,20 +6389,20 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718" + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/9a67fcf320561e96f94d62bbe0e169ac534a5718", - "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718", + "url": "https://api.github.com/repos/symfony/serializer/zipball/10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", "shasum": "" }, "require": { @@ -6432,7 +6471,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.10" + "source": "https://github.com/symfony/serializer/tree/v6.4.12" }, "funding": [ { @@ -6448,7 +6487,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T13:13:26+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/service-contracts", @@ -6597,16 +6636,16 @@ }, { "name": "symfony/string", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ccf9b30251719567bfd46494138327522b9a9446" + "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", - "reference": "ccf9b30251719567bfd46494138327522b9a9446", + "url": "https://api.github.com/repos/symfony/string/zipball/f8a1ccebd0997e16112dfecfd74220b78e5b284b", + "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b", "shasum": "" }, "require": { @@ -6663,7 +6702,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.10" + "source": "https://github.com/symfony/string/tree/v6.4.12" }, "funding": [ { @@ -6679,7 +6718,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:21:14+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/translation-contracts", @@ -6761,16 +6800,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "9bcb26445b9d4ef1087c389234bf33fb00e10ea6" + "reference": "09c0df13f822a1b80c5972ca1aa9eeb1288e1194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9bcb26445b9d4ef1087c389234bf33fb00e10ea6", - "reference": "9bcb26445b9d4ef1087c389234bf33fb00e10ea6", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/09c0df13f822a1b80c5972ca1aa9eeb1288e1194", + "reference": "09c0df13f822a1b80c5972ca1aa9eeb1288e1194", "shasum": "" }, "require": { @@ -6850,7 +6889,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.9" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.12" }, "funding": [ { @@ -6866,20 +6905,20 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-09-15T06:35:36+00:00" }, { "name": "symfony/twig-bundle", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65" + "reference": "4e63369647e3924e110b37337c6a58aac3086ad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", - "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/4e63369647e3924e110b37337c6a58aac3086ad4", + "reference": "4e63369647e3924e110b37337c6a58aac3086ad4", "shasum": "" }, "require": { @@ -6934,7 +6973,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v6.4.8" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.12" }, "funding": [ { @@ -6950,20 +6989,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-08T12:30:05+00:00" }, { "name": "symfony/validator", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd" + "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", - "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", + "url": "https://api.github.com/repos/symfony/validator/zipball/6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0", + "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0", "shasum": "" }, "require": { @@ -7031,7 +7070,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.10" + "source": "https://github.com/symfony/validator/tree/v6.4.12" }, "funding": [ { @@ -7047,20 +7086,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" + "reference": "ee14c8254a480913268b1e3b1cba8045ed122694" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ee14c8254a480913268b1e3b1cba8045ed122694", + "reference": "ee14c8254a480913268b1e3b1cba8045ed122694", "shasum": "" }, "require": { @@ -7116,7 +7155,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.11" }, "funding": [ { @@ -7132,7 +7171,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-08-30T16:03:21+00:00" }, { "name": "symfony/var-exporter", @@ -7296,16 +7335,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -7348,7 +7387,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.8" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -7364,28 +7403,28 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "twig/twig", - "version": "v3.10.3", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22" + "symfony/polyfill-php81": "^1.29" }, "require-dev": { "psr/container": "^1.0|^2.0", @@ -7431,7 +7470,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.10.3" + "source": "https://github.com/twigphp/Twig/tree/v3.14.0" }, "funding": [ { @@ -7443,7 +7482,7 @@ "type": "tidelift" } ], - "time": "2024-05-16T10:04:27+00:00" + "time": "2024-09-09T17:55:12+00:00" }, { "name": "webmozart/assert", @@ -7627,26 +7666,26 @@ }, { "name": "composer/pcre", - "version": "3.2.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.8" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan": "^1.11.10", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^8 || ^9" }, @@ -7686,7 +7725,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -7702,28 +7741,28 @@ "type": "tidelift" } ], - "time": "2024-07-25T09:36:02+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -7767,7 +7806,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -7783,7 +7822,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -7963,16 +8002,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -8012,7 +8051,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -8020,20 +8059,20 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.61.1", + "version": "v3.64.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" + "reference": "58dd9c931c785a79739310aef5178928305ffa67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", "shasum": "" }, "require": { @@ -8115,7 +8154,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" }, "funding": [ { @@ -8123,7 +8162,7 @@ "type": "github" } ], - "time": "2024-07-31T14:33:15+00:00" + "time": "2024-08-30T23:09:38+00:00" }, { "name": "masterminds/html5", @@ -8254,16 +8293,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.2.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", "shasum": "" }, "require": { @@ -8306,9 +8345,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" }, - "time": "2024-07-01T20:03:41+00:00" + "time": "2024-09-15T16:40:33+00:00" }, { "name": "phar-io/manifest", @@ -8430,35 +8469,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -8467,7 +8506,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -8496,7 +8535,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -8504,7 +8543,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -8749,16 +8788,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { @@ -8773,7 +8812,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -8832,7 +8871,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -8848,7 +8887,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "react/cache", @@ -10478,16 +10517,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "105b56a0305d219349edeb60a800082eca864e4b" + "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/105b56a0305d219349edeb60a800082eca864e4b", - "reference": "105b56a0305d219349edeb60a800082eca864e4b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/9d307ecbcb917001692be333cdc58f474fdb37f0", + "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0", "shasum": "" }, "require": { @@ -10525,7 +10564,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.8" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.12" }, "funding": [ { @@ -10541,20 +10580,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-15T06:35:36+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.60.0", + "version": "v1.61.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "c305a02a22974670f359d4274c9431e1a191f559" + "reference": "a3b7f14d349f8f44ed752d4dde2263f77510cc18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c305a02a22974670f359d4274c9431e1a191f559", - "reference": "c305a02a22974670f359d4274c9431e1a191f559", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/a3b7f14d349f8f44ed752d4dde2263f77510cc18", + "reference": "a3b7f14d349f8f44ed752d4dde2263f77510cc18", "shasum": "" }, "require": { @@ -10617,7 +10656,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.60.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.61.0" }, "funding": [ { @@ -10633,7 +10672,7 @@ "type": "tidelift" } ], - "time": "2024-06-10T06:03:18+00:00" + "time": "2024-08-29T22:50:23+00:00" }, { "name": "symfony/options-resolver", @@ -10704,16 +10743,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1" + "reference": "e876eb90e32a8fc4c4911d458e09f88d65877d1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e823122d31935eb711e2767c31f3d71cb0b87fb1", - "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e876eb90e32a8fc4c4911d458e09f88d65877d1c", + "reference": "e876eb90e32a8fc4c4911d458e09f88d65877d1c", "shasum": "" }, "require": { @@ -10766,7 +10805,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.4" }, "funding": [ { @@ -10782,20 +10821,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/process", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "url": "https://api.github.com/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", "shasum": "" }, "require": { @@ -10827,7 +10866,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.8" + "source": "https://github.com/symfony/process/tree/v6.4.12" }, "funding": [ { @@ -10843,7 +10882,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "theseer/tokenizer", From a071abaefa45335b90b165921b508935144ad788 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 25 Sep 2024 14:47:53 +0200 Subject: [PATCH 031/191] Code styling fixes --- src/ApiResource/Version.php | 2 +- src/Command/BenzinaPumpCommand.php | 2 +- src/Command/GatewaysSetupCommand.php | 2 +- src/Command/SystemVarsListCommand.php | 2 +- src/DependencyInjection/SystemVariablesLoader.php | 2 +- src/Entity/Money.php | 2 +- src/Filter/OrderedLikeFilter.php | 4 ++-- src/Filter/ResourceVersionResourceFilter.php | 2 +- src/Filter/ResourceVersionResourceIdFilter.php | 2 +- src/Filter/UserQueryFilter.php | 4 ++-- src/Library/Benzina/Benzina.php | 2 +- src/Library/Benzina/Pump/CheckoutsPump.php | 2 +- src/Library/Benzina/Pump/ProjectsPump.php | 2 +- src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php | 8 ++++---- src/Library/Benzina/Pump/UsersPump.php | 2 +- src/Library/Benzina/Stream/PdoStream.php | 2 +- src/Library/Economy/Payment/PaypalGateway.php | 2 +- src/Library/Economy/Payment/StripeGateway.php | 2 +- src/Service/Auth/AuthService.php | 2 +- src/State/GatewayCheckoutProcessor.php | 2 +- src/State/GatewayStateProvider.php | 2 +- src/State/ResourceVersionStateProvider.php | 2 +- src/State/UserStateProcessor.php | 2 +- src/State/UserTokenLoginProcessor.php | 2 +- src/Validator/GatewayNameValidator.php | 2 +- 25 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php index 103e649..b94e121 100644 --- a/src/ApiResource/Version.php +++ b/src/ApiResource/Version.php @@ -23,7 +23,7 @@ class Version { public function __construct( private readonly LogEntry $log, - private readonly object $entity + private readonly object $entity, ) { } diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 6d5a2c0..4335d44 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -19,7 +19,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class BenzinaPumpCommand extends Command { public function __construct( - private Benzina $benzina + private Benzina $benzina, ) { parent::__construct(); } diff --git a/src/Command/GatewaysSetupCommand.php b/src/Command/GatewaysSetupCommand.php index 1bb23f6..b9bf75c 100644 --- a/src/Command/GatewaysSetupCommand.php +++ b/src/Command/GatewaysSetupCommand.php @@ -20,7 +20,7 @@ class GatewaysSetupCommand extends Command private const FAILURE_MESSAGE = 'Could not setup Gateways. Please review the error.'; public function __construct( - private GatewayLocator $gatewayLocator + private GatewayLocator $gatewayLocator, ) { parent::__construct(); } diff --git a/src/Command/SystemVarsListCommand.php b/src/Command/SystemVarsListCommand.php index 030b3af..1be82e6 100644 --- a/src/Command/SystemVarsListCommand.php +++ b/src/Command/SystemVarsListCommand.php @@ -16,7 +16,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class SystemVarsListCommand extends Command { public function __construct( - private SystemVariableRepository $systemVarRepository + private SystemVariableRepository $systemVarRepository, ) { parent::__construct(); } diff --git a/src/DependencyInjection/SystemVariablesLoader.php b/src/DependencyInjection/SystemVariablesLoader.php index 9bec1a0..a877679 100644 --- a/src/DependencyInjection/SystemVariablesLoader.php +++ b/src/DependencyInjection/SystemVariablesLoader.php @@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceEx final class SystemVariablesLoader implements EnvVarLoaderInterface { public function __construct( - private SystemVariableRepository $systemVarRepository + private SystemVariableRepository $systemVarRepository, ) { } diff --git a/src/Entity/Money.php b/src/Entity/Money.php index 0396268..8b31d07 100644 --- a/src/Entity/Money.php +++ b/src/Entity/Money.php @@ -33,7 +33,7 @@ class Money public function __construct( int $amount, - string $currency + string $currency, ) { $this->amount = $amount; $this->currency = $currency; diff --git a/src/Filter/OrderedLikeFilter.php b/src/Filter/OrderedLikeFilter.php index 856e044..27197ce 100644 --- a/src/Filter/OrderedLikeFilter.php +++ b/src/Filter/OrderedLikeFilter.php @@ -17,7 +17,7 @@ final class OrderedLikeFilter extends AbstractFilter protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, - protected ?NameConverterInterface $nameConverter = null + protected ?NameConverterInterface $nameConverter = null, ) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); } @@ -29,7 +29,7 @@ final class OrderedLikeFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { if ( !$this->isPropertyEnabled($property, $resourceClass) diff --git a/src/Filter/ResourceVersionResourceFilter.php b/src/Filter/ResourceVersionResourceFilter.php index a331e12..3fddcdf 100644 --- a/src/Filter/ResourceVersionResourceFilter.php +++ b/src/Filter/ResourceVersionResourceFilter.php @@ -31,7 +31,7 @@ final class ResourceVersionResourceFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { return; } diff --git a/src/Filter/ResourceVersionResourceIdFilter.php b/src/Filter/ResourceVersionResourceIdFilter.php index f8a9804..e7521bc 100644 --- a/src/Filter/ResourceVersionResourceIdFilter.php +++ b/src/Filter/ResourceVersionResourceIdFilter.php @@ -17,7 +17,7 @@ final class ResourceVersionResourceIdFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { return; } diff --git a/src/Filter/UserQueryFilter.php b/src/Filter/UserQueryFilter.php index 07409c0..e179cb3 100644 --- a/src/Filter/UserQueryFilter.php +++ b/src/Filter/UserQueryFilter.php @@ -17,7 +17,7 @@ final class UserQueryFilter extends AbstractFilter protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, - protected ?NameConverterInterface $nameConverter = null + protected ?NameConverterInterface $nameConverter = null, ) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); } @@ -29,7 +29,7 @@ final class UserQueryFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { if ( !$this->isPropertyEnabled($property, $resourceClass) diff --git a/src/Library/Benzina/Benzina.php b/src/Library/Benzina/Benzina.php index 881ef84..a958519 100644 --- a/src/Library/Benzina/Benzina.php +++ b/src/Library/Benzina/Benzina.php @@ -11,7 +11,7 @@ class Benzina private array $availablePumps = []; public function __construct( - iterable $instanceof + iterable $instanceof, ) { $this->availablePumps = \iterator_to_array($instanceof); } diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index db37971..4fc9842 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -69,7 +69,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private UserRepository $userRepository, private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { } diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index 62538c9..859f2b3 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -96,7 +96,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface public function __construct( private UserRepository $userRepository, - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { } diff --git a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php b/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php index 920648f..58cb192 100644 --- a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php +++ b/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php @@ -19,7 +19,7 @@ use Doctrine\ORM\EntityManagerInterface; trait ProgressivePumpTrait { public function __construct( - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { } @@ -33,7 +33,7 @@ trait ProgressivePumpTrait public function getPumped( string $entityClass, array $pumpingBatch, - array $matchCriteria + array $matchCriteria, ): array { $repository = $this->entityManager->getRepository($entityClass); @@ -63,7 +63,7 @@ trait ProgressivePumpTrait public function isPumped( array $pumpingRecord, array $pumpedBatch, - array $matchCriteria + array $matchCriteria, ): bool { if ( isset($this->config) @@ -89,7 +89,7 @@ trait ProgressivePumpTrait public function getPumpedRecord( array $pumpingRecord, array $pumpedBatch, - array $matchCriteria + array $matchCriteria, ): mixed { $entityKey = \array_keys($matchCriteria)[0]; $pumpingKey = $matchCriteria[$entityKey]; diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index def0707..15848bd 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -51,7 +51,7 @@ class UsersPump extends AbstractPump implements PumpInterface ]; public function __construct( - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { } diff --git a/src/Library/Benzina/Stream/PdoStream.php b/src/Library/Benzina/Stream/PdoStream.php index 1b8dd11..ad8c40f 100644 --- a/src/Library/Benzina/Stream/PdoStream.php +++ b/src/Library/Benzina/Stream/PdoStream.php @@ -13,7 +13,7 @@ class PdoStream implements StreamInterface public function __construct( string $database, private string $tablename, - private int $sizeOfBatch = 99 + private int $sizeOfBatch = 99, ) { $parsedUrl = parse_url($database); $dbdata = [ diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 8959ffa..916423e 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -50,7 +50,7 @@ class PaypalGateway implements GatewayInterface private HttpClientInterface $httpClient, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, - private GatewayCheckoutRepository $checkoutRepository + private GatewayCheckoutRepository $checkoutRepository, ) { $this->cache = new FilesystemAdapter(); diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index b1f269c..39ababa 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -31,7 +31,7 @@ class StripeGateway implements GatewayInterface private RouterInterface $router, private GatewayCheckoutRepository $gatewayCheckoutRepository, private EntityManagerInterface $entityManager, - private IriConverterInterface $iriConverter + private IriConverterInterface $iriConverter, ) { $this->stripe = new StripeClient($stripeApiKey); } diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index fb3561e..5d3795f 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -12,7 +12,7 @@ class AuthService private array $config; public function __construct( - private string $appSecret + private string $appSecret, ) { } diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index 5b865ed..4d47c78 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -16,7 +16,7 @@ class GatewayCheckoutProcessor implements ProcessorInterface #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $innerProcessor, private GatewayLocator $gatewayLocator, - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { } diff --git a/src/State/GatewayStateProvider.php b/src/State/GatewayStateProvider.php index ce91eeb..0ae771b 100644 --- a/src/State/GatewayStateProvider.php +++ b/src/State/GatewayStateProvider.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class GatewayStateProvider implements ProviderInterface { public function __construct( - private GatewayLocator $gateways + private GatewayLocator $gateways, ) { } diff --git a/src/State/ResourceVersionStateProvider.php b/src/State/ResourceVersionStateProvider.php index a7482df..3698d73 100644 --- a/src/State/ResourceVersionStateProvider.php +++ b/src/State/ResourceVersionStateProvider.php @@ -18,7 +18,7 @@ class ResourceVersionStateProvider implements ProviderInterface private LogEntryRepository $versionRepository; public function __construct( - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { $this->versionRepository = $this->entityManager->getRepository(LogEntry::class); } diff --git a/src/State/UserStateProcessor.php b/src/State/UserStateProcessor.php index 00f2086..5632ec3 100644 --- a/src/State/UserStateProcessor.php +++ b/src/State/UserStateProcessor.php @@ -13,7 +13,7 @@ class UserStateProcessor implements ProcessorInterface { public function __construct( private ProcessorInterface $innerProcessor, - private UserPasswordHasherInterface $userPasswordHasher + private UserPasswordHasherInterface $userPasswordHasher, ) { } diff --git a/src/State/UserTokenLoginProcessor.php b/src/State/UserTokenLoginProcessor.php index ac3f4e5..3c55e44 100644 --- a/src/State/UserTokenLoginProcessor.php +++ b/src/State/UserTokenLoginProcessor.php @@ -20,7 +20,7 @@ class UserTokenLoginProcessor implements ProcessorInterface private AuthService $authService, private UserRepository $userRepository, private EntityManagerInterface $entityManager, - private UserPasswordHasherInterface $userPasswordHasher + private UserPasswordHasherInterface $userPasswordHasher, ) { } diff --git a/src/Validator/GatewayNameValidator.php b/src/Validator/GatewayNameValidator.php index caec861..4dfe265 100644 --- a/src/Validator/GatewayNameValidator.php +++ b/src/Validator/GatewayNameValidator.php @@ -10,7 +10,7 @@ use Symfony\Component\Validator\Constraints\ChoiceValidator; class GatewayNameValidator extends ChoiceValidator { public function __construct( - private GatewayLocator $gateways + private GatewayLocator $gateways, ) { } From b994cf607ed6e28232819e8bb9547a653cc70205 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 14:03:17 +0200 Subject: [PATCH 032/191] Move gateway checkout redirect urls generation to dedicated gateway checkout service --- .../Economy/Payment/GatewayInterface.php | 3 -- src/Library/Economy/Payment/PaypalGateway.php | 14 +++------ src/Library/Economy/Payment/StripeGateway.php | 18 ++++-------- src/Service/GatewayCheckoutService.php | 29 +++++++++++++++++++ 4 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 src/Service/GatewayCheckoutService.php diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index 23fa1d2..7575e5c 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -23,9 +23,6 @@ use Symfony\Component\HttpFoundation\Response; */ interface GatewayInterface { - public const RESPONSE_TYPE_SUCCESS = 'success'; - public const RESPONSE_TYPE_FAILURE = 'failure'; - /** * @return string A short, unique, descriptive string for this Gateway */ diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 916423e..ff4690b 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -3,7 +3,6 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Controller\GatewaysController; use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; @@ -11,6 +10,7 @@ use App\Entity\GatewayCheckoutLink; use App\Entity\GatewayCheckoutLinkType; use App\Entity\GatewayCheckoutStatus; use App\Repository\GatewayCheckoutRepository; +use App\Service\GatewayCheckoutService; use Brick\Money\Money as BrickMoney; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -50,6 +50,7 @@ class PaypalGateway implements GatewayInterface private HttpClientInterface $httpClient, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, + private GatewayCheckoutService $checkoutService, private GatewayCheckoutRepository $checkoutRepository, ) { $this->cache = new FilesystemAdapter(); @@ -169,7 +170,7 @@ class PaypalGateway implements GatewayInterface { $token = $request->query->get('token'); - if ($request->query->get('type') !== self::RESPONSE_TYPE_SUCCESS) { + if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $token)); } @@ -264,14 +265,7 @@ class PaypalGateway implements GatewayInterface private function getPaypalPaymentSource(): array { - $successUrl = $this->router->generate( - GatewaysController::REDIRECT, - [ - 'type' => self::RESPONSE_TYPE_SUCCESS, - 'gateway' => $this->getName(), - ], - RouterInterface::ABSOLUTE_URL - ); + $successUrl = $this->checkoutService->generateRedirectUrl($this->getName()); return [ 'paypal' => [ diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 39ababa..ab8a64e 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -3,7 +3,6 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Controller\GatewaysController; use App\Entity\AccountingTransaction; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; @@ -11,6 +10,7 @@ use App\Entity\GatewayCheckoutLink; use App\Entity\GatewayCheckoutLinkType; use App\Entity\GatewayCheckoutStatus; use App\Repository\GatewayCheckoutRepository; +use App\Service\GatewayCheckoutService; use Doctrine\ORM\EntityManagerInterface; use Stripe\Checkout\Session as StripeSession; use Stripe\StripeClient; @@ -29,7 +29,8 @@ class StripeGateway implements GatewayInterface private string $stripeApiKey, private string $stripeWebhookSecret, private RouterInterface $router, - private GatewayCheckoutRepository $gatewayCheckoutRepository, + private GatewayCheckoutService $checkoutService, + private GatewayCheckoutRepository $checkoutRepository, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, ) { @@ -43,14 +44,7 @@ class StripeGateway implements GatewayInterface public function sendData(GatewayCheckout $checkout): GatewayCheckout { - $successUrl = $this->router->generate( - GatewaysController::REDIRECT, - [ - 'type' => self::RESPONSE_TYPE_SUCCESS, - 'gateway' => $this->getName(), - ], - RouterInterface::ABSOLUTE_URL - ); + $successUrl = $this->checkoutService->generateRedirectUrl($this->getName()); $session = $this->stripe->checkout->sessions->create([ 'customer_email' => $checkout->getOrigin()->getUser()->getEmail(), @@ -103,7 +97,7 @@ class StripeGateway implements GatewayInterface $sessionId = $request->query->get('session_id'); $session = $this->stripe->checkout->sessions->retrieve($sessionId); - $checkout = $this->gatewayCheckoutRepository->findOneBy( + $checkout = $this->checkoutRepository->findOneBy( ['gatewayReference' => $sessionId] ); @@ -111,7 +105,7 @@ class StripeGateway implements GatewayInterface throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId)); } - if ($request->query->get('type') !== self::RESPONSE_TYPE_SUCCESS) { + if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { return $checkout; } diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php new file mode 100644 index 0000000..9c6ae9b --- /dev/null +++ b/src/Service/GatewayCheckoutService.php @@ -0,0 +1,29 @@ +router->generate( + GatewaysController::REDIRECT, + [ + 'type' => $type, + 'gateway' => $gateway, + ], + RouterInterface::ABSOLUTE_URL + ); + } +} From 2d4e5817e6f4ba5f1d15bc556e9eb7d2b592434a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 14:04:18 +0200 Subject: [PATCH 033/191] Avoid stripe resuccess on redirect handling --- src/Library/Economy/Payment/StripeGateway.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index ab8a64e..fd2b1b0 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -105,6 +105,10 @@ class StripeGateway implements GatewayInterface throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId)); } + if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + return $checkout; + } + if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { return $checkout; } From 61ab4d0d2f8d549ca46767dff541fa65a49bf34e Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 14:06:08 +0200 Subject: [PATCH 034/191] Move gateway reference generation to gateway checkout service --- src/Library/Economy/Payment/PaypalGateway.php | 7 +------ src/Service/GatewayCheckoutService.php | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index ff4690b..c8d44cc 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -224,18 +224,13 @@ class PaypalGateway implements GatewayInterface ]; } - private function getPaypalReference(GatewayCheckout $checkout, GatewayCharge $charge): string - { - return sprintf('CO%d-CH%d', $checkout->getId(), $charge->getId()); - } - private function getPaypalPurchaseUnits(GatewayCheckout $checkout): array { $units = []; foreach ($checkout->getCharges() as $charge) { $money = $this->getPaypalMoney($charge); - $reference = $this->getPaypalReference($checkout, $charge); + $reference = $this->checkoutService->getGatewayReference($checkout, $charge); $units[] = [ 'reference_id' => $reference, diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 9c6ae9b..0da2e67 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -3,6 +3,8 @@ namespace App\Service; use App\Controller\GatewaysController; +use App\Entity\GatewayCharge; +use App\Entity\GatewayCheckout; use Symfony\Component\Routing\RouterInterface; class GatewayCheckoutService @@ -12,8 +14,7 @@ class GatewayCheckoutService public function __construct( private RouterInterface $router, - ) { - } + ) {} public function generateRedirectUrl(string $gateway, string $type = self::RESPONSE_TYPE_SUCCESS, array $parameters = []): string { @@ -26,4 +27,15 @@ class GatewayCheckoutService RouterInterface::ABSOLUTE_URL ); } + + public function getGatewayReference(GatewayCheckout $checkout, GatewayCharge $charge): string + { + return sprintf( + 'AO%d-CO%d-CH%d-AT%d', + $checkout->getOrigin()->getId(), + $checkout->getId(), + $charge->getId(), + $charge->getTarget()->getId() + ); + } } From 76ad89da61c8b85377d75dc63909c30b1c7361a7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 14:15:25 +0200 Subject: [PATCH 035/191] Move paypal order fetch out of redirect handler --- src/Library/Economy/Payment/PaypalGateway.php | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index c8d44cc..6bcb21f 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -144,63 +144,48 @@ class PaypalGateway implements GatewayInterface return $checkout; } - private function handleSuccess(GatewayCheckout $checkout): GatewayCheckout + public function fetchPaypalOrder(string $orderId): array { - $checkout->setStatus(GatewayCheckoutStatus::Charged); + $request = $this->httpClient->request( + Request::METHOD_GET, + sprintf('/v2/checkout/orders/%s', $orderId), + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ] + ); - foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); - - $this->entityManager->persist($transaction); - - $charge->setTransaction($transaction); - - $this->entityManager->persist($charge); + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); } - $this->entityManager->flush(); - - return $checkout; + return \json_decode($request->getContent(), true); } public function handleRedirect(Request $request): RedirectResponse { - $token = $request->query->get('token'); + $orderId = $request->query->get('token'); - if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { - throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $token)); + if (!$orderId || $request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { + throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $orderId)); } - $requestUri = sprintf('/v2/checkout/orders/%s', $token); - $request = $this->httpClient->request(Request::METHOD_GET, $requestUri, [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - ]); - - if ($request->getStatusCode() !== Response::HTTP_OK) { - throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $token)); - } - - $session = \json_decode($request->getContent(), true); $checkout = $this->checkoutRepository->findOneBy( - ['gatewayReference' => $token] + ['gatewayReference' => $orderId] ); if ($checkout === null) { - throw new \Exception(sprintf("PayPal checkout '%s' exists but no GatewayCheckout with that reference was found.", $token)); + throw new \Exception(sprintf("PayPal checkout '%s' exists but no GatewayCheckout with that reference was found.", $orderId)); } if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } - if (!\in_array($session['status'], [self::PAYPAL_STATUS_APPROVED, self::PAYPAL_STATUS_COMPLETED])) { - throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $token)); - } + $order = $this->fetchPaypalOrder($orderId); - $checkout = $this->handleSuccess($checkout); + if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) { + throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId)); + } // TO-DO: This should redirect the user to a GUI return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); From 2aac4f7bc8e95d9818d2de00c9828885545a0b31 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 15:28:11 +0200 Subject: [PATCH 036/191] Refactor single gateway reference to be able to hold different gateway ids --- src/Entity/GatewayCheckout.php | 54 +++++++++----- src/Entity/GatewayCheckoutId.php | 72 +++++++++++++++++++ .../GatewayCheckoutIdRepository.php | 43 +++++++++++ 3 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 src/Entity/GatewayCheckoutId.php create mode 100644 src/Repository/GatewayCheckoutIdRepository.php diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index ff2f218..397b685 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -72,12 +72,13 @@ class GatewayCheckout private ?string $gateway = null; /** - * An external identifier provided by the Gateway for the payment. + * The IDs provided by the Gateway for this checkout. + * + * @var Collection */ #[API\ApiProperty(writable: false)] - #[API\ApiFilter(SearchFilter::class)] - #[ORM\Column(length: 255, nullable: true)] - private ?string $gatewayReference = null; + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutId::class, cascade: ['persist'])] + private Collection $gatewayIds; /** * The URLs provided by the Gateway for this checkout. @@ -86,7 +87,7 @@ class GatewayCheckout */ #[API\ApiProperty(writable: false)] #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutLink::class, cascade: ['persist'])] - private Collection $links; + private Collection $gatewayLinks; /** * GatewayCheckout was migrated from an invest record in Goteo v3 platform. @@ -112,7 +113,8 @@ class GatewayCheckout { $this->charges = new ArrayCollection(); $this->status = GatewayCheckoutStatus::Pending; - $this->links = new ArrayCollection(); + $this->gatewayLinks = new ArrayCollection(); + $this->gatewayIds = new ArrayCollection(); } public function getId(): ?int @@ -180,14 +182,32 @@ class GatewayCheckout return $this; } - public function getGatewayReference(): ?string + /** + * @return Collection + */ + public function getGatewayIds(): Collection { - return $this->gatewayReference; + return $this->gatewayIds; } - public function setGatewayReference(string $gatewayReference): static + public function addGatewayId(GatewayCheckoutId $gatewayId): static { - $this->gatewayReference = $gatewayReference; + if (!$this->gatewayIds->contains($gatewayId)) { + $this->gatewayIds->add($gatewayId); + $gatewayId->setCheckout($this); + } + + return $this; + } + + public function removeGatewayId(GatewayCheckoutId $gatewayId): static + { + if ($this->gatewayIds->removeElement($gatewayId)) { + // set the owning side to null (unless already changed) + if ($gatewayId->getCheckout() === $this) { + $gatewayId->setCheckout(null); + } + } return $this; } @@ -195,24 +215,24 @@ class GatewayCheckout /** * @return Collection */ - public function getLinks(): Collection + public function getGatewayLinks(): Collection { - return $this->links; + return $this->gatewayLinks; } - public function addLink(GatewayCheckoutLink $link): static + public function addGatewayLink(GatewayCheckoutLink $link): static { - if (!$this->links->contains($link)) { - $this->links->add($link); + if (!$this->gatewayLinks->contains($link)) { + $this->gatewayLinks->add($link); $link->setCheckout($this); } return $this; } - public function removeLink(GatewayCheckoutLink $link): static + public function removeGatewayLink(GatewayCheckoutLink $link): static { - if ($this->links->removeElement($link)) { + if ($this->gatewayLinks->removeElement($link)) { // set the owning side to null (unless already changed) if ($link->getCheckout() === $this) { $link->setCheckout(null); diff --git a/src/Entity/GatewayCheckoutId.php b/src/Entity/GatewayCheckoutId.php new file mode 100644 index 0000000..ec41ffa --- /dev/null +++ b/src/Entity/GatewayCheckoutId.php @@ -0,0 +1,72 @@ +id; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(string $value): static + { + $this->value = $value; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getCheckout(): ?GatewayCheckout + { + return $this->checkout; + } + + public function setCheckout(?GatewayCheckout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } +} diff --git a/src/Repository/GatewayCheckoutIdRepository.php b/src/Repository/GatewayCheckoutIdRepository.php new file mode 100644 index 0000000..e46a568 --- /dev/null +++ b/src/Repository/GatewayCheckoutIdRepository.php @@ -0,0 +1,43 @@ + + */ +class GatewayCheckoutIdRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, GatewayCheckoutId::class); + } + + // /** + // * @return GatewayCheckoutId[] Returns an array of GatewayCheckoutId objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('g.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?GatewayCheckoutId + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} From 060a7dbdee5979e650cda7fcb740278667596b7d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 15:28:25 +0200 Subject: [PATCH 037/191] Move generic checkout logic to checkout service --- src/Library/Economy/Payment/StripeGateway.php | 39 +++++------------- src/Service/GatewayCheckoutService.php | 40 +++++++++++++++++-- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index fd2b1b0..8c2e0e0 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -3,9 +3,9 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Entity\AccountingTransaction; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; +use App\Entity\GatewayCheckoutId; use App\Entity\GatewayCheckoutLink; use App\Entity\GatewayCheckoutLinkType; use App\Entity\GatewayCheckoutStatus; @@ -44,8 +44,6 @@ class StripeGateway implements GatewayInterface public function sendData(GatewayCheckout $checkout): GatewayCheckout { - $successUrl = $this->checkoutService->generateRedirectUrl($this->getName()); - $session = $this->stripe->checkout->sessions->create([ 'customer_email' => $checkout->getOrigin()->getUser()->getEmail(), 'mode' => $this->getStripeMode($checkout), @@ -54,7 +52,7 @@ class StripeGateway implements GatewayInterface // is not properly sent to Stripe and the redirection fails, // that's why we add the session_id template variable like this. // https://docs.stripe.com/payments/checkout/custom-success-page?lang=php#modify-the-success-url - 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $successUrl), + 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)), ]); $link = new GatewayCheckoutLink(); @@ -64,30 +62,13 @@ class StripeGateway implements GatewayInterface $link->setMethod(Request::METHOD_GET); $link->setType(GatewayCheckoutLinkType::Payment); - $checkout->addLink($link); - $checkout->setGatewayReference($session->id); + $checkout->addGatewayLink($link); - return $checkout; - } + $sessionId = new GatewayCheckoutId(); + $sessionId->setValue($session->id); + $sessionId->setTitle('Stripe Checkout Session ID'); - private function handleSuccess(GatewayCheckout $checkout): GatewayCheckout - { - $checkout->setStatus(GatewayCheckoutStatus::Charged); - - foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); - - $this->entityManager->persist($transaction); - - $charge->setTransaction($transaction); - - $this->entityManager->persist($charge); - } - - $this->entityManager->flush(); + $checkout->addGatewayId($sessionId); return $checkout; } @@ -97,9 +78,7 @@ class StripeGateway implements GatewayInterface $sessionId = $request->query->get('session_id'); $session = $this->stripe->checkout->sessions->retrieve($sessionId); - $checkout = $this->checkoutRepository->findOneBy( - ['gatewayReference' => $sessionId] - ); + $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)); @@ -117,7 +96,7 @@ class StripeGateway implements GatewayInterface return $checkout; } - $checkout = $this->handleSuccess($checkout); + $checkout = $this->checkoutService->chargeCheckout($checkout); // TO-DO: This should redirect the user to a GUI return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 0da2e67..10c460f 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -3,8 +3,11 @@ namespace App\Service; use App\Controller\GatewaysController; +use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; +use App\Entity\GatewayCheckoutStatus; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Routing\RouterInterface; class GatewayCheckoutService @@ -14,15 +17,22 @@ class GatewayCheckoutService public function __construct( private RouterInterface $router, - ) {} + private EntityManagerInterface $entityManager, + ) { + } - public function generateRedirectUrl(string $gateway, string $type = self::RESPONSE_TYPE_SUCCESS, array $parameters = []): string - { + public function generateRedirectUrl( + GatewayCheckout $checkout, + string $type = self::RESPONSE_TYPE_SUCCESS, + array $parameters = [], + ): string { return $this->router->generate( GatewaysController::REDIRECT, [ 'type' => $type, - 'gateway' => $gateway, + 'gateway' => $checkout->getGateway(), + 'checkoutId' => $checkout->getId(), + ...$parameters, ], RouterInterface::ABSOLUTE_URL ); @@ -38,4 +48,26 @@ class GatewayCheckoutService $charge->getTarget()->getId() ); } + + public function chargeCheckout(GatewayCheckout $checkout): GatewayCheckout + { + $checkout->setStatus(GatewayCheckoutStatus::Charged); + + foreach ($checkout->getCharges() as $charge) { + $transaction = new AccountingTransaction(); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $this->entityManager->persist($transaction); + + $charge->setTransaction($transaction); + + $this->entityManager->persist($charge); + } + + $this->entityManager->flush(); + + return $checkout; + } } From a0ab60afa6fcf23035e0e76b38df0a824b447ccb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 26 Sep 2024 15:28:47 +0200 Subject: [PATCH 038/191] Be able to complete PayPal payment --- src/Library/Economy/Payment/PaypalGateway.php | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 6bcb21f..1e149d2 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -3,9 +3,9 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; -use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; +use App\Entity\GatewayCheckoutId; use App\Entity\GatewayCheckoutLink; use App\Entity\GatewayCheckoutLinkType; use App\Entity\GatewayCheckoutStatus; @@ -125,7 +125,11 @@ class PaypalGateway implements GatewayInterface throw new \Exception($content['message']); } - $checkout->setGatewayReference($content['id']); + $orderId = new GatewayCheckoutId(); + $orderId->setValue($content['id']); + $orderId->setTitle('PayPal Order ID'); + + $checkout->addGatewayId($orderId); foreach ($content['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) @@ -138,7 +142,7 @@ class PaypalGateway implements GatewayInterface $link->setMethod($linkData['method']); $link->setType($linkType); - $checkout->addLink($link); + $checkout->addGatewayLink($link); } return $checkout; @@ -161,6 +165,33 @@ class PaypalGateway implements GatewayInterface return \json_decode($request->getContent(), true); } + private function capturePaypalOrder(array $order) + { + $link = \array_filter($order['links'], function ($order) { + return $order['rel'] === 'capture'; + }); + + if (empty($link)) { + throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); + } + + $link = \array_pop($link); + $request = $this->httpClient->request( + $link['method'], + $link['href'], + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'headers' => ['Content-Type' => 'application/json'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_CREATED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); + } + + return \json_decode($request->getContent(), true); + } + public function handleRedirect(Request $request): RedirectResponse { $orderId = $request->query->get('token'); @@ -169,9 +200,7 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $orderId)); } - $checkout = $this->checkoutRepository->findOneBy( - ['gatewayReference' => $orderId] - ); + $checkout = $this->checkoutRepository->find($request->query->get('checkoutId')); if ($checkout === null) { throw new \Exception(sprintf("PayPal checkout '%s' exists but no GatewayCheckout with that reference was found.", $orderId)); @@ -187,6 +216,20 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId)); } + $capture = $this->capturePaypalOrder($order); + + if ($capture['status'] !== self::PAYPAL_STATUS_COMPLETED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); + } + + $captureId = new GatewayCheckoutId(); + $captureId->setValue($capture['purchase_units'][0]['payments']['captures'][0]['id']); + $captureId->setTitle('PayPal Transaction ID'); + + $checkout->addGatewayId($captureId); + + $checkout = $this->checkoutService->chargeCheckout($checkout); + // TO-DO: This should redirect the user to a GUI return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } @@ -219,6 +262,7 @@ class PaypalGateway implements GatewayInterface $units[] = [ 'reference_id' => $reference, + 'custom_id' => $reference, 'items' => [ [ 'name' => $charge::MESSAGE_STATEMENT, @@ -243,14 +287,12 @@ class PaypalGateway implements GatewayInterface return $units; } - private function getPaypalPaymentSource(): array + private function getPaypalPaymentSource(GatewayCheckout $checkout): array { - $successUrl = $this->checkoutService->generateRedirectUrl($this->getName()); - return [ 'paypal' => [ 'experience_context' => [ - 'return_url' => $successUrl, + 'return_url' => $this->checkoutService->generateRedirectUrl($checkout), ], ], ]; From fa481abb083a82b7626dbd4ede09cc9d77b39602 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 1 Oct 2024 12:13:15 +0200 Subject: [PATCH 039/191] Rename gateway main method to process --- src/Library/Economy/Payment/GatewayInterface.php | 2 +- src/Library/Economy/Payment/StripeGateway.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Library/Economy/Payment/GatewayInterface.php b/src/Library/Economy/Payment/GatewayInterface.php index 7575e5c..c9cb0bc 100644 --- a/src/Library/Economy/Payment/GatewayInterface.php +++ b/src/Library/Economy/Payment/GatewayInterface.php @@ -35,7 +35,7 @@ interface GatewayInterface * * @return GatewayCheckout The GatewayCheckout updated with the data given by the gateway */ - public function sendData(GatewayCheckout $checkout): GatewayCheckout; + public function process(GatewayCheckout $checkout): GatewayCheckout; /** * When a user is redirected by the gateway we must handle the redirection diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 8c2e0e0..31150bf 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -42,7 +42,7 @@ class StripeGateway implements GatewayInterface return 'stripe'; } - public function sendData(GatewayCheckout $checkout): GatewayCheckout + public function process(GatewayCheckout $checkout): GatewayCheckout { $session = $this->stripe->checkout->sessions->create([ 'customer_email' => $checkout->getOrigin()->getUser()->getEmail(), From 47041aada9825fe23d3442aa9bd43f7d00c9db3c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 1 Oct 2024 12:13:30 +0200 Subject: [PATCH 040/191] Reorder PaypalGateway methods order --- src/Library/Economy/Payment/PaypalGateway.php | 166 +++++++++--------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 1e149d2..e03a453 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -70,45 +70,7 @@ class PaypalGateway implements GatewayInterface return 'paypal'; } - public function setHttpClient(HttpClientInterface $httpClient): static - { - $this->httpClient = $httpClient; - - return $this; - } - - /** - * Calls the PayPal API to generate an OAuth2 token. - * - * @return array The API response body - */ - public function generateAuthToken(): array - { - $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ - 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], - 'body' => 'grant_type=client_credentials', - ]); - - return $response->toArray(); - } - - /** - * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. - * - * @return array The OAuth2 token data - */ - public function getAuthToken(): array - { - return $this->cache->get($this->getName(), function (ItemInterface $item): array { - $tokenData = $this->generateAuthToken(); - - $item->expiresAfter($tokenData['expires_in']); - - return $tokenData; - }); - } - - public function sendData(GatewayCheckout $checkout): GatewayCheckout + public function process(GatewayCheckout $checkout): GatewayCheckout { $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ 'auth_bearer' => $this->getAuthToken()['access_token'], @@ -148,50 +110,6 @@ class PaypalGateway implements GatewayInterface return $checkout; } - public function fetchPaypalOrder(string $orderId): array - { - $request = $this->httpClient->request( - Request::METHOD_GET, - sprintf('/v2/checkout/orders/%s', $orderId), - [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - ] - ); - - if ($request->getStatusCode() !== Response::HTTP_OK) { - throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); - } - - return \json_decode($request->getContent(), true); - } - - private function capturePaypalOrder(array $order) - { - $link = \array_filter($order['links'], function ($order) { - return $order['rel'] === 'capture'; - }); - - if (empty($link)) { - throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); - } - - $link = \array_pop($link); - $request = $this->httpClient->request( - $link['method'], - $link['href'], - [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - 'headers' => ['Content-Type' => 'application/json'], - ] - ); - - if ($request->getStatusCode() !== Response::HTTP_CREATED) { - throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); - } - - return \json_decode($request->getContent(), true); - } - public function handleRedirect(Request $request): RedirectResponse { $orderId = $request->query->get('token'); @@ -239,6 +157,88 @@ class PaypalGateway implements GatewayInterface return new Response(); } + public function setHttpClient(HttpClientInterface $httpClient): static + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Calls the PayPal API to generate an OAuth2 token. + * + * @return array The API response body + */ + public function generateAuthToken(): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ + 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], + 'body' => 'grant_type=client_credentials', + ]); + + return $response->toArray(); + } + + /** + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. + * + * @return array The OAuth2 token data + */ + public function getAuthToken(): array + { + return $this->cache->get($this->getName(), function (ItemInterface $item): array { + $tokenData = $this->generateAuthToken(); + + $item->expiresAfter($tokenData['expires_in']); + + return $tokenData; + }); + } + + public function fetchPaypalOrder(string $orderId): array + { + $request = $this->httpClient->request( + Request::METHOD_GET, + sprintf('/v2/checkout/orders/%s', $orderId), + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); + } + + return \json_decode($request->getContent(), true); + } + + private function capturePaypalOrder(array $order) + { + $link = \array_filter($order['links'], function ($order) { + return $order['rel'] === 'capture'; + }); + + if (empty($link)) { + throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); + } + + $link = \array_pop($link); + $request = $this->httpClient->request( + $link['method'], + $link['href'], + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'headers' => ['Content-Type' => 'application/json'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_CREATED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); + } + + return \json_decode($request->getContent(), true); + } + private function getPaypalMoney(GatewayCharge $charge): array { $brick = BrickMoney::ofMinor( From 6932bbff95c32df76cb107cb7ec3b670864e6edc Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 1 Oct 2024 12:20:23 +0200 Subject: [PATCH 041/191] Improve order of data checks on paypal redirect handling --- src/Library/Economy/Payment/PaypalGateway.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index e03a453..b3bbc95 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -112,22 +112,28 @@ class PaypalGateway implements GatewayInterface public function handleRedirect(Request $request): RedirectResponse { - $orderId = $request->query->get('token'); - - if (!$orderId || $request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { - throw new \Exception(sprintf("PayPal checkout '%s' was not completed successfully.", $orderId)); + if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { + throw new \Exception(sprintf("Checkout was not completed successfully.")); } - $checkout = $this->checkoutRepository->find($request->query->get('checkoutId')); + $checkoutId = $request->query->get('checkoutId'); + + $checkout = $this->checkoutRepository->find($checkoutId); if ($checkout === null) { - throw new \Exception(sprintf("PayPal checkout '%s' exists but no GatewayCheckout with that reference was found.", $orderId)); + throw new \Exception(sprintf("GatewayCheckout '%s' could not be found.", $checkoutId)); } if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); } + $orderId = $request->query->get('token'); + + if (!$orderId) { + throw new \Exception(sprintf("PayPal checkout order ID not provided by the gateway.")); + } + $order = $this->fetchPaypalOrder($orderId); if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) { From 303f6cce0d8fcf0911725e4af63cacda62bd01b9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:12:23 +0200 Subject: [PATCH 042/191] Rename GatewayCheckoutId to GatewayTracking --- src/Entity/GatewayCheckout.php | 33 ++++++++++--------- ...ewayCheckoutId.php => GatewayTracking.php} | 10 +++--- ...tory.php => GatewayTrackingRepository.php} | 12 +++---- 3 files changed, 28 insertions(+), 27 deletions(-) rename src/Entity/{GatewayCheckoutId.php => GatewayTracking.php} (83%) rename src/Repository/{GatewayCheckoutIdRepository.php => GatewayTrackingRepository.php} (70%) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 397b685..3127188 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -72,13 +72,14 @@ class GatewayCheckout private ?string $gateway = null; /** - * The IDs provided by the Gateway for this checkout. + * A list of tracking codes provided by the Gateway for this checkout.\ + * e.g: Order ID, Payment Capture ID, Checkout Session Token * - * @var Collection + * @var Collection */ #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutId::class, cascade: ['persist'])] - private Collection $gatewayIds; + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayTracking::class, cascade: ['persist'])] + private Collection $gatewayTrackings; /** * The URLs provided by the Gateway for this checkout. @@ -114,7 +115,7 @@ class GatewayCheckout $this->charges = new ArrayCollection(); $this->status = GatewayCheckoutStatus::Pending; $this->gatewayLinks = new ArrayCollection(); - $this->gatewayIds = new ArrayCollection(); + $this->gatewayTrackings = new ArrayCollection(); } public function getId(): ?int @@ -183,29 +184,29 @@ class GatewayCheckout } /** - * @return Collection + * @return Collection */ - public function getGatewayIds(): Collection + public function getGatewayTrackings(): Collection { - return $this->gatewayIds; + return $this->gatewayTrackings; } - public function addGatewayId(GatewayCheckoutId $gatewayId): static + public function addGatewayTracking(GatewayTracking $gatewayTracking): static { - if (!$this->gatewayIds->contains($gatewayId)) { - $this->gatewayIds->add($gatewayId); - $gatewayId->setCheckout($this); + if (!$this->gatewayTrackings->contains($gatewayTracking)) { + $this->gatewayTrackings->add($gatewayTracking); + $gatewayTracking->setCheckout($this); } return $this; } - public function removeGatewayId(GatewayCheckoutId $gatewayId): static + public function removeGatewayTracking(GatewayTracking $gatewayTracking): static { - if ($this->gatewayIds->removeElement($gatewayId)) { + if ($this->gatewayTrackings->removeElement($gatewayTracking)) { // set the owning side to null (unless already changed) - if ($gatewayId->getCheckout() === $this) { - $gatewayId->setCheckout(null); + if ($gatewayTracking->getCheckout() === $this) { + $gatewayTracking->setCheckout(null); } } diff --git a/src/Entity/GatewayCheckoutId.php b/src/Entity/GatewayTracking.php similarity index 83% rename from src/Entity/GatewayCheckoutId.php rename to src/Entity/GatewayTracking.php index ec41ffa..0799870 100644 --- a/src/Entity/GatewayCheckoutId.php +++ b/src/Entity/GatewayTracking.php @@ -2,11 +2,11 @@ namespace App\Entity; -use App\Repository\GatewayCheckoutIdRepository; +use App\Repository\GatewayTrackingRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: GatewayCheckoutIdRepository::class)] -class GatewayCheckoutId +#[ORM\Entity(repositoryClass: GatewayTrackingRepository::class)] +class GatewayTracking { #[ORM\Id] #[ORM\GeneratedValue] @@ -14,13 +14,13 @@ class GatewayCheckoutId private ?int $id = null; /** - * The ID given by the Gateway. + * The tracking number given by the Gateway. */ #[ORM\Column(length: 255)] private ?string $value = null; /** - * A descriptive title for the ID type. + * A descriptive title for the tracking number. */ #[ORM\Column(length: 255)] private ?string $title = null; diff --git a/src/Repository/GatewayCheckoutIdRepository.php b/src/Repository/GatewayTrackingRepository.php similarity index 70% rename from src/Repository/GatewayCheckoutIdRepository.php rename to src/Repository/GatewayTrackingRepository.php index e46a568..f687a3f 100644 --- a/src/Repository/GatewayCheckoutIdRepository.php +++ b/src/Repository/GatewayTrackingRepository.php @@ -2,22 +2,22 @@ namespace App\Repository; -use App\Entity\GatewayCheckoutId; +use App\Entity\GatewayTracking; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @extends ServiceEntityRepository + * @extends ServiceEntityRepository */ -class GatewayCheckoutIdRepository extends ServiceEntityRepository +class GatewayTrackingRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, GatewayCheckoutId::class); + parent::__construct($registry, GatewayTracking::class); } // /** - // * @return GatewayCheckoutId[] Returns an array of GatewayCheckoutId objects + // * @return GatewayTracking[] Returns an array of GatewayTracking objects // */ // public function findByExampleField($value): array // { @@ -31,7 +31,7 @@ class GatewayCheckoutIdRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?GatewayCheckoutId + // public function findOneBySomeField($value): ?GatewayTracking // { // return $this->createQueryBuilder('g') // ->andWhere('g.exampleField = :val') From f4acb952a04e5831ef35f412554359aac6672ab2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:20:05 +0200 Subject: [PATCH 043/191] Rename GatewayCheckoutLink to GatewayLink --- src/Entity/GatewayCheckout.php | 81 ++++++++++--------- ...atewayCheckoutLink.php => GatewayLink.php} | 14 ++-- ...eckoutLinkType.php => GatewayLinkType.php} | 2 +- ...pository.php => GatewayLinkRepository.php} | 12 +-- 4 files changed, 55 insertions(+), 54 deletions(-) rename src/Entity/{GatewayCheckoutLink.php => GatewayLink.php} (85%) rename src/Entity/{GatewayCheckoutLinkType.php => GatewayLinkType.php} (90%) rename src/Repository/{GatewayCheckoutLinkRepository.php => GatewayLinkRepository.php} (70%) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 3127188..c97afc3 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -71,9 +71,19 @@ class GatewayCheckout #[ORM\Column(length: 255)] private ?string $gateway = null; + /** + * A list of URLs provided by the Gateway for this checkout.\ + * e.g: Fulfill payment, API resource address. + * + * @var Collection + */ + #[API\ApiProperty(writable: false)] + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayLink::class, cascade: ['persist'])] + private Collection $gatewayLinks; + /** * A list of tracking codes provided by the Gateway for this checkout.\ - * e.g: Order ID, Payment Capture ID, Checkout Session Token + * e.g: Order ID, Payment Capture ID, Checkout Session Token. * * @var Collection */ @@ -81,15 +91,6 @@ class GatewayCheckout #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayTracking::class, cascade: ['persist'])] private Collection $gatewayTrackings; - /** - * The URLs provided by the Gateway for this checkout. - * - * @var Collection - */ - #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayCheckoutLink::class, cascade: ['persist'])] - private Collection $gatewayLinks; - /** * GatewayCheckout was migrated from an invest record in Goteo v3 platform. */ @@ -183,6 +184,36 @@ class GatewayCheckout return $this; } + /** + * @return Collection + */ + public function getGatewayLinks(): Collection + { + return $this->gatewayLinks; + } + + public function addGatewayLink(GatewayLink $link): static + { + if (!$this->gatewayLinks->contains($link)) { + $this->gatewayLinks->add($link); + $link->setCheckout($this); + } + + return $this; + } + + public function removeGatewayLink(GatewayLink $link): static + { + if ($this->gatewayLinks->removeElement($link)) { + // set the owning side to null (unless already changed) + if ($link->getCheckout() === $this) { + $link->setCheckout(null); + } + } + + return $this; + } + /** * @return Collection */ @@ -213,36 +244,6 @@ class GatewayCheckout return $this; } - /** - * @return Collection - */ - public function getGatewayLinks(): Collection - { - return $this->gatewayLinks; - } - - public function addGatewayLink(GatewayCheckoutLink $link): static - { - if (!$this->gatewayLinks->contains($link)) { - $this->gatewayLinks->add($link); - $link->setCheckout($this); - } - - return $this; - } - - public function removeGatewayLink(GatewayCheckoutLink $link): static - { - if ($this->gatewayLinks->removeElement($link)) { - // set the owning side to null (unless already changed) - if ($link->getCheckout() === $this) { - $link->setCheckout(null); - } - } - - return $this; - } - public function isMigrated(): ?bool { return $this->migrated; diff --git a/src/Entity/GatewayCheckoutLink.php b/src/Entity/GatewayLink.php similarity index 85% rename from src/Entity/GatewayCheckoutLink.php rename to src/Entity/GatewayLink.php index 9ff892a..86db39b 100644 --- a/src/Entity/GatewayCheckoutLink.php +++ b/src/Entity/GatewayLink.php @@ -3,12 +3,12 @@ namespace App\Entity; use ApiPlatform\Metadata as API; -use App\Repository\GatewayCheckoutLinkRepository; +use App\Repository\GatewayLinkRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: GatewayCheckoutLinkRepository::class)] -class GatewayCheckoutLink +#[ORM\Entity(repositoryClass: GatewayLinkRepository::class)] +class GatewayLink { #[ORM\Id] #[ORM\GeneratedValue] @@ -40,8 +40,8 @@ class GatewayCheckoutLink * `debug` links are for developers and platform maintainers to get useful information about the checkout.\ * `payment` links are for end-users who must visit this link to complete the checkout. */ - #[ORM\Column(enumType: GatewayCheckoutLinkType::class)] - private ?GatewayCheckoutLinkType $type = null; + #[ORM\Column(enumType: GatewayLinkType::class)] + private ?GatewayLinkType $type = null; #[API\ApiProperty(readable: false)] #[ORM\ManyToOne(inversedBy: 'links')] @@ -89,12 +89,12 @@ class GatewayCheckoutLink return $this; } - public function getType(): ?GatewayCheckoutLinkType + public function getType(): ?GatewayLinkType { return $this->type; } - public function setType(GatewayCheckoutLinkType $type): static + public function setType(GatewayLinkType $type): static { $this->type = $type; diff --git a/src/Entity/GatewayCheckoutLinkType.php b/src/Entity/GatewayLinkType.php similarity index 90% rename from src/Entity/GatewayCheckoutLinkType.php rename to src/Entity/GatewayLinkType.php index 5f00cc0..f2384b4 100644 --- a/src/Entity/GatewayCheckoutLinkType.php +++ b/src/Entity/GatewayLinkType.php @@ -2,7 +2,7 @@ namespace App\Entity; -enum GatewayCheckoutLinkType: string +enum GatewayLinkType: string { /** * A 'debug' type link indicates a link that is helpful to developers and platform maintainers to get info about the checkout. diff --git a/src/Repository/GatewayCheckoutLinkRepository.php b/src/Repository/GatewayLinkRepository.php similarity index 70% rename from src/Repository/GatewayCheckoutLinkRepository.php rename to src/Repository/GatewayLinkRepository.php index de67518..3207933 100644 --- a/src/Repository/GatewayCheckoutLinkRepository.php +++ b/src/Repository/GatewayLinkRepository.php @@ -2,22 +2,22 @@ namespace App\Repository; -use App\Entity\GatewayCheckoutLink; +use App\Entity\GatewayLink; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @extends ServiceEntityRepository + * @extends ServiceEntityRepository */ -class GatewayCheckoutLinkRepository extends ServiceEntityRepository +class GatewayLinkRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, GatewayCheckoutLink::class); + parent::__construct($registry, GatewayLink::class); } // /** - // * @return GatewayCheckoutLink[] Returns an array of GatewayCheckoutLink objects + // * @return GatewayLink[] Returns an array of GatewayLink objects // */ // public function findByExampleField($value): array // { @@ -31,7 +31,7 @@ class GatewayCheckoutLinkRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?GatewayCheckoutLink + // public function findOneBySomeField($value): ?GatewayLink // { // return $this->createQueryBuilder('g') // ->andWhere('g.exampleField = :val') From cdbcf11f85061df613ebc3eaac387655b36d7e92 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:22:52 +0200 Subject: [PATCH 044/191] Update StripeGateway with GatewayLink and GatewayTracking --- src/Library/Economy/Payment/StripeGateway.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 31150bf..27da965 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -5,9 +5,9 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutId; -use App\Entity\GatewayCheckoutLink; -use App\Entity\GatewayCheckoutLinkType; +use App\Entity\GatewayTracking; +use App\Entity\GatewayLink; +use App\Entity\GatewayLinkType; use App\Entity\GatewayCheckoutStatus; use App\Repository\GatewayCheckoutRepository; use App\Service\GatewayCheckoutService; @@ -55,20 +55,20 @@ class StripeGateway implements GatewayInterface 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)), ]); - $link = new GatewayCheckoutLink(); + $link = new GatewayLink(); $link->setHref($session->url); $link->setRel('approve'); $link->setMethod(Request::METHOD_GET); - $link->setType(GatewayCheckoutLinkType::Payment); + $link->setType(GatewayLinkType::Payment); $checkout->addGatewayLink($link); - $sessionId = new GatewayCheckoutId(); - $sessionId->setValue($session->id); - $sessionId->setTitle('Stripe Checkout Session ID'); + $tracking = new GatewayTracking(); + $tracking->setValue($session->id); + $tracking->setTitle('Stripe Checkout Session ID'); - $checkout->addGatewayId($sessionId); + $checkout->addGatewayTracking($tracking); return $checkout; } From 881389000e83d3672e39258863acaf1a560b9075 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:30:07 +0200 Subject: [PATCH 045/191] Add basic paypal webhook handling --- .env | 1 + config/services.yaml | 1 + src/Library/Economy/Payment/PaypalGateway.php | 69 ++++++++++++++----- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 9664fd1..d34937d 100644 --- a/.env +++ b/.env @@ -47,4 +47,5 @@ STRIPE_WEBHOOK_SECRET="" ###> App\Library\Economy\Payment\PaypalGateway ### PAYPAL_CLIENT_ID="" PAYPAL_CLIENT_SECRET="" +PAYPAL_WEBHOOK_ID="WEBHOOK_ID" ###< App\Library\Economy\Payment\PaypalGateway ### diff --git a/config/services.yaml b/config/services.yaml index baf01e9..87350c7 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -63,6 +63,7 @@ services: - '%env(APP_ENV)%' - '%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/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index b3bbc95..c5ef9b4 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -5,9 +5,9 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutId; -use App\Entity\GatewayCheckoutLink; -use App\Entity\GatewayCheckoutLinkType; +use App\Entity\GatewayTracking; +use App\Entity\GatewayLink; +use App\Entity\GatewayLinkType; use App\Entity\GatewayCheckoutStatus; use App\Repository\GatewayCheckoutRepository; use App\Service\GatewayCheckoutService; @@ -15,6 +15,7 @@ use Brick\Money\Money as BrickMoney; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\HttpClient\HttpOptions; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -35,6 +36,9 @@ class PaypalGateway implements GatewayInterface public const PAYPAL_STATUS_APPROVED = 'APPROVED'; public const PAYPAL_STATUS_COMPLETED = 'COMPLETED'; + /** @see https://developer.paypal.com/api/rest/webhooks/event-names/#orders */ + public const PAYPAL_CHECKOUT_ORDER_COMPLETED = 'CHECKOUT.ORDER.COMPLETED'; + /** * @see https://developer.paypal.com/docs/api/orders/v2/ */ @@ -46,6 +50,7 @@ class PaypalGateway implements GatewayInterface private string $appEnv, private string $paypalClientId, private string $paypalClientSecret, + private string $paypalWebhookId, private RouterInterface $router, private HttpClientInterface $httpClient, private EntityManagerInterface $entityManager, @@ -87,18 +92,18 @@ class PaypalGateway implements GatewayInterface throw new \Exception($content['message']); } - $orderId = new GatewayCheckoutId(); - $orderId->setValue($content['id']); - $orderId->setTitle('PayPal Order ID'); + $tracking = new GatewayTracking(); + $tracking->setValue($content['id']); + $tracking->setTitle('PayPal Order ID'); - $checkout->addGatewayId($orderId); + $checkout->addGatewayTracking($tracking); foreach ($content['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) - ? GatewayCheckoutLinkType::Payment - : GatewayCheckoutLinkType::Debug; + ? GatewayLinkType::Payment + : GatewayLinkType::Debug; - $link = new GatewayCheckoutLink(); + $link = new GatewayLink(); $link->setHref($linkData['href']); $link->setRel($linkData['rel']); $link->setMethod($linkData['method']); @@ -113,7 +118,7 @@ class PaypalGateway implements GatewayInterface public function handleRedirect(Request $request): RedirectResponse { if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) { - throw new \Exception(sprintf("Checkout was not completed successfully.")); + throw new \Exception(sprintf('Checkout was not completed successfully.')); } $checkoutId = $request->query->get('checkoutId'); @@ -131,7 +136,7 @@ class PaypalGateway implements GatewayInterface $orderId = $request->query->get('token'); if (!$orderId) { - throw new \Exception(sprintf("PayPal checkout order ID not provided by the gateway.")); + throw new \Exception(sprintf('PayPal checkout order ID not provided by the gateway.')); } $order = $this->fetchPaypalOrder($orderId); @@ -146,11 +151,11 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); } - $captureId = new GatewayCheckoutId(); - $captureId->setValue($capture['purchase_units'][0]['payments']['captures'][0]['id']); - $captureId->setTitle('PayPal Transaction ID'); + $tracking = new GatewayTracking(); + $tracking->setValue($capture['purchase_units'][0]['payments']['captures'][0]['id']); + $tracking->setTitle('PayPal Transaction ID'); - $checkout->addGatewayId($captureId); + $checkout->addGatewayTracking($tracking); $checkout = $this->checkoutService->chargeCheckout($checkout); @@ -160,9 +165,41 @@ class PaypalGateway implements GatewayInterface public function handleWebhook(Request $request): Response { + $isSignatureValid = $this->verifyWebhookSignature($request); + if (!$isSignatureValid) { + return new JsonResponse([ + 'status' => 'ERROR', + 'message' => 'Could not verify webhook signature', + 'requestBody' => $request->getContent(), + 'requestHeaders' => $request->headers->all(), + ], Response::HTTP_ACCEPTED); + } + + $event = \json_decode($request->getContent(), true); + switch ($event['event_type']) { + default: + return new Response('Event not supported', Response::HTTP_ACCEPTED); + } + return new Response(); } + private function verifyWebhookSignature(Request $request): bool + { + $headers = $request->headers; + + return \openssl_verify( + implode('|', [ + $headers->get('paypal-transmission-id'), + $headers->get('paypal-transmission-type'), + $this->paypalWebhookId, + \crc32($request->getContent()), + ]), + \base64_decode($headers->get('paypal-transmission-sig')), + \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) + ) === 1; + } + public function setHttpClient(HttpClientInterface $httpClient): static { $this->httpClient = $httpClient; From e4a58cef6d5d5aba1db92fd1578dc0a98d1577a3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:44:49 +0200 Subject: [PATCH 046/191] Fix interface main method bug --- src/State/GatewayCheckoutProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index 4d47c78..fef90d2 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -32,7 +32,7 @@ class GatewayCheckoutProcessor implements ProcessorInterface $checkout = $this->innerProcessor->process($data, $operation, $uriVariables, $context); $gateway = $this->gatewayLocator->getGatewayOf($checkout); - $checkout = $gateway->sendData($checkout); + $checkout = $gateway->process($checkout); return $this->innerProcessor->process($data, $operation, $uriVariables, $context); } From 20f566fe9b9bf9dc133ad9bae2bb283ff34f81f2 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 13:45:02 +0200 Subject: [PATCH 047/191] Fix bug only saving one tracking number --- src/Library/Economy/Payment/PaypalGateway.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index c5ef9b4..1e63816 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -151,11 +151,13 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); } - $tracking = new GatewayTracking(); - $tracking->setValue($capture['purchase_units'][0]['payments']['captures'][0]['id']); - $tracking->setTitle('PayPal Transaction ID'); + foreach ($capture['purchase_units'] as $purchaseUnit) { + $tracking = new GatewayTracking(); + $tracking->setValue($purchaseUnit['payments']['captures'][0]['id']); + $tracking->setTitle('PayPal Transaction ID'); - $checkout->addGatewayTracking($tracking); + $checkout->addGatewayTracking($tracking); + } $checkout = $this->checkoutService->chargeCheckout($checkout); From e2a326c78ba37ef536cfe1394096b7db8b8683ca Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:03:08 +0200 Subject: [PATCH 048/191] Allow to find one GatewayCheckout by a GatewayTracking --- src/Repository/GatewayCheckoutRepository.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Repository/GatewayCheckoutRepository.php b/src/Repository/GatewayCheckoutRepository.php index 050535a..ea8e91d 100644 --- a/src/Repository/GatewayCheckoutRepository.php +++ b/src/Repository/GatewayCheckoutRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\GatewayCheckout; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,6 +22,20 @@ class GatewayCheckoutRepository extends ServiceEntityRepository parent::__construct($registry, GatewayCheckout::class); } + /** + * @return GatewayCheckout|null + */ + public function findOneByTracking($value): array + { + return $this->createQueryBuilder('g') + ->join('g.gatewayTrackings', 'gt', Join::WITH, 'gt.checkout = g.id') + ->andWhere('gt.value = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + // /** // * @return GatewayCheckout[] Returns an array of GatewayCheckout objects // */ From 4a535d74e709b996d06ae092b75d0b7bc60f9109 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:09:06 +0200 Subject: [PATCH 049/191] Find by GatewayCheckout by tracking title and value --- src/Library/Economy/Payment/PaypalGateway.php | 32 +++++++++++++++++-- src/Repository/GatewayCheckoutRepository.php | 4 ++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 1e63816..bdf0a60 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -37,13 +37,16 @@ class PaypalGateway implements GatewayInterface public const PAYPAL_STATUS_COMPLETED = 'COMPLETED'; /** @see https://developer.paypal.com/api/rest/webhooks/event-names/#orders */ - public const PAYPAL_CHECKOUT_ORDER_COMPLETED = 'CHECKOUT.ORDER.COMPLETED'; + public const PAYPAL_EVENT_ORDER_COMPLETED = 'CHECKOUT.ORDER.COMPLETED'; /** * @see https://developer.paypal.com/docs/api/orders/v2/ */ private const PAYPAL_ORDER_INTENT = 'CAPTURE'; + public const TRACKING_TITLE_ORDER = 'PayPal Order ID'; + public const TRACKING_TITLE_TRANSACTION = 'PayPal Transaction ID'; + private CacheInterface $cache; public function __construct( @@ -94,7 +97,7 @@ class PaypalGateway implements GatewayInterface $tracking = new GatewayTracking(); $tracking->setValue($content['id']); - $tracking->setTitle('PayPal Order ID'); + $tracking->setTitle(self::TRACKING_TITLE_ORDER); $checkout->addGatewayTracking($tracking); @@ -154,7 +157,7 @@ class PaypalGateway implements GatewayInterface foreach ($capture['purchase_units'] as $purchaseUnit) { $tracking = new GatewayTracking(); $tracking->setValue($purchaseUnit['payments']['captures'][0]['id']); - $tracking->setTitle('PayPal Transaction ID'); + $tracking->setTitle(self::TRACKING_TITLE_TRANSACTION); $checkout->addGatewayTracking($tracking); } @@ -179,6 +182,8 @@ class PaypalGateway implements GatewayInterface $event = \json_decode($request->getContent(), true); switch ($event['event_type']) { + case self::PAYPAL_EVENT_ORDER_COMPLETED: + return $this->handleOrderCompleted($event); default: return new Response('Event not supported', Response::HTTP_ACCEPTED); } @@ -186,6 +191,27 @@ class PaypalGateway implements GatewayInterface return new Response(); } + private function handleOrderCompleted(array $event) + { + $orderId = $event['resource']['id']; + + $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); + } + + foreach ($event['resource']['purchase_units'] as $purchaseUnit) { + $tracking = new GatewayTracking(); + $tracking->setValue($purchaseUnit['payments']['captures'][0]['id']); + $tracking->setTitle(self::TRACKING_TITLE_TRANSACTION); + + $checkout->addGatewayTracking($tracking); + } + + $checkout = $this->checkoutService->chargeCheckout($checkout); + } + private function verifyWebhookSignature(Request $request): bool { $headers = $request->headers; diff --git a/src/Repository/GatewayCheckoutRepository.php b/src/Repository/GatewayCheckoutRepository.php index ea8e91d..9d8ee48 100644 --- a/src/Repository/GatewayCheckoutRepository.php +++ b/src/Repository/GatewayCheckoutRepository.php @@ -25,12 +25,14 @@ class GatewayCheckoutRepository extends ServiceEntityRepository /** * @return GatewayCheckout|null */ - public function findOneByTracking($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() ; From 086d918ee46dd0f213cf7852f549682fa90da49f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:34:26 +0200 Subject: [PATCH 050/191] Move paypal request logic to dedicated service --- config/services.yaml | 4 +- .../Economy/Payment/PaypalGatewayService.php | 144 ++++++++++++++++++ ...yTest.php => PaypalGatewayServiceTest.php} | 12 +- 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/Library/Economy/Payment/PaypalGatewayService.php rename tests/Library/Economy/Payment/{PaypalGatewayTest.php => PaypalGatewayServiceTest.php} (77%) diff --git a/config/services.yaml b/config/services.yaml index 87350c7..1e06935 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -58,9 +58,9 @@ services: - '%env(STRIPE_API_KEY)%' - '%env(STRIPE_WEBHOOK_SECRET)%' - App\Library\Economy\Payment\PaypalGateway: + App\Library\Economy\Payment\PaypalGatewayService: arguments: - - '%env(APP_ENV)%' + - '%env(PAYPAL_API_ADDRESS)%' - '%env(PAYPAL_CLIENT_ID)%' - '%env(PAYPAL_CLIENT_SECRET)%' - '%env(PAYPAL_WEBHOOK_ID)%' diff --git a/src/Library/Economy/Payment/PaypalGatewayService.php b/src/Library/Economy/Payment/PaypalGatewayService.php new file mode 100644 index 0000000..f077531 --- /dev/null +++ b/src/Library/Economy/Payment/PaypalGatewayService.php @@ -0,0 +1,144 @@ +cache = new FilesystemAdapter(); + + $httpOptions = new HttpOptions(); + $httpOptions->setBaseUri($paypalApiAddress); + + $this->setHttpClient($httpClient->withOptions($httpOptions->toArray())); + } + + public function setHttpClient(HttpClientInterface $httpClient): static + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Calls the PayPal API to generate an OAuth2 token. + * + * @return array The API response body + */ + public function generateAuthToken(): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ + 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], + 'body' => 'grant_type=client_credentials', + ]); + + return $response->toArray(); + } + + /** + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. + * + * @return array The OAuth2 token data + */ + public function getAuthToken(): array + { + return $this->cache->get($this->paypalApiAddress, function (ItemInterface $item): array { + $tokenData = $this->generateAuthToken(); + + $item->expiresAfter($tokenData['expires_in']); + + return $tokenData; + }); + } + + public function postOrder(array $order): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'json' => $order, + ]); + + $content = \json_decode($response->getContent(), true); + if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) { + throw new \Exception($content['message']); + } + + return $content; + } + + public function getOrder(string $orderId): array + { + $request = $this->httpClient->request( + Request::METHOD_GET, + sprintf('/v2/checkout/orders/%s', $orderId), + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); + } + + return \json_decode($request->getContent(), true); + } + + public function captureOrder(array $order): array + { + $link = \array_filter($order['links'], function ($order) { + return $order['rel'] === 'capture'; + }); + + if (empty($link)) { + throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); + } + + $link = \array_pop($link); + $request = $this->httpClient->request( + $link['method'], + $link['href'], + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'headers' => ['Content-Type' => 'application/json'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_CREATED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); + } + + return \json_decode($request->getContent(), true); + } + + public function verifyWebhook(Request $request): bool + { + $headers = $request->headers; + + return \openssl_verify( + implode('|', [ + $headers->get('paypal-transmission-id'), + $headers->get('paypal-transmission-type'), + $this->paypalWebhookId, + \crc32($request->getContent()), + ]), + \base64_decode($headers->get('paypal-transmission-sig')), + \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) + ) === 1; + } +} diff --git a/tests/Library/Economy/Payment/PaypalGatewayTest.php b/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php similarity index 77% rename from tests/Library/Economy/Payment/PaypalGatewayTest.php rename to tests/Library/Economy/Payment/PaypalGatewayServiceTest.php index 441d36c..618282b 100644 --- a/tests/Library/Economy/Payment/PaypalGatewayTest.php +++ b/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php @@ -2,20 +2,20 @@ namespace App\Tests\Library\Economy\Payment; -use App\Library\Economy\Payment\PaypalGateway; +use App\Library\Economy\Payment\PaypalGatewayService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -class PaypalGatewayTest extends KernelTestCase +class PaypalGatewayServiceTest extends KernelTestCase { - private PaypalGateway $paypal; + private PaypalGatewayService $paypalService; public function setUp(): void { self::bootKernel(); - $this->paypal = static::getContainer()->get(PaypalGateway::class); + $this->paypalService = static::getContainer()->get(PaypalGatewayService::class); } public function testAuthenticates(): void @@ -27,9 +27,9 @@ class PaypalGatewayTest extends KernelTestCase ]); $httpClient = new MockHttpClient($mockResponse, 'https://testapi.paypal.com'); - $this->paypal = $this->paypal->setHttpClient($httpClient); + $this->paypalService = $this->paypalService->setHttpClient($httpClient); - $responseData = $this->paypal->generateAuthToken(); + $responseData = $this->paypalService->generateAuthToken(); $this->assertSame('POST', $mockResponse->getRequestMethod()); $this->assertSame('https://testapi.paypal.com/v1/oauth2/token', $mockResponse->getRequestUrl()); From 77f615e07e73ee2e4c7c744be0e0667abb16a4ee Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:34:55 +0200 Subject: [PATCH 051/191] Added PAYPAL_API_ADDRESS env var --- .env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env b/.env index d34937d..5d6a1b5 100644 --- a/.env +++ b/.env @@ -45,6 +45,8 @@ STRIPE_WEBHOOK_SECRET="" ###< App\Library\Economy\Payment\StripeGateway ### ###> App\Library\Economy\Payment\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" From 12bb760209548942f793028866565c116820273c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:35:13 +0200 Subject: [PATCH 052/191] Remove PaypalGatewayService code --- src/Library/Economy/Payment/PaypalGateway.php | 148 ++---------------- 1 file changed, 10 insertions(+), 138 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index bdf0a60..7ac7446 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -13,16 +13,11 @@ use App\Repository\GatewayCheckoutRepository; use App\Service\GatewayCheckoutService; use Brick\Money\Money as BrickMoney; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\HttpClient\HttpOptions; 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; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @see https://developer.paypal.com/studio/checkout/standard/integrate @@ -47,30 +42,14 @@ class PaypalGateway implements GatewayInterface public const TRACKING_TITLE_ORDER = 'PayPal Order ID'; public const TRACKING_TITLE_TRANSACTION = 'PayPal Transaction ID'; - private CacheInterface $cache; - public function __construct( - private string $appEnv, - private string $paypalClientId, - private string $paypalClientSecret, - private string $paypalWebhookId, + private PaypalGatewayService $paypal, private RouterInterface $router, - private HttpClientInterface $httpClient, private EntityManagerInterface $entityManager, private IriConverterInterface $iriConverter, private GatewayCheckoutService $checkoutService, private GatewayCheckoutRepository $checkoutRepository, ) { - $this->cache = new FilesystemAdapter(); - - $httpOptions = new HttpOptions(); - $httpOptions->setBaseUri( - $appEnv === 'prod' - ? self::PAYPAL_API_ADDRESS_LIVE - : self::PAYPAL_API_ADDRESS_SANDBOX - ); - - $this->setHttpClient($httpClient->withOptions($httpOptions->toArray())); } public static function getName(): string @@ -80,28 +59,19 @@ class PaypalGateway implements GatewayInterface public function process(GatewayCheckout $checkout): GatewayCheckout { - $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - 'json' => [ - 'intent' => self::PAYPAL_ORDER_INTENT, - 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), - 'payment_source' => $this->getPaypalPaymentSource($checkout), - ], + $order = $this->paypal->postOrder([ + 'intent' => self::PAYPAL_ORDER_INTENT, + 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), + 'payment_source' => $this->getPaypalPaymentSource($checkout), ]); - $content = json_decode($response->getContent(), true); - - if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) { - throw new \Exception($content['message']); - } - $tracking = new GatewayTracking(); - $tracking->setValue($content['id']); + $tracking->setValue($order['id']); $tracking->setTitle(self::TRACKING_TITLE_ORDER); $checkout->addGatewayTracking($tracking); - foreach ($content['links'] as $linkData) { + foreach ($order['links'] as $linkData) { $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) ? GatewayLinkType::Payment : GatewayLinkType::Debug; @@ -142,13 +112,13 @@ class PaypalGateway implements GatewayInterface throw new \Exception(sprintf('PayPal checkout order ID not provided by the gateway.')); } - $order = $this->fetchPaypalOrder($orderId); + $order = $this->paypal->getOrder($orderId); if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) { throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId)); } - $capture = $this->capturePaypalOrder($order); + $capture = $this->paypal->captureOrder($order); if ($capture['status'] !== self::PAYPAL_STATUS_COMPLETED) { throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); @@ -170,7 +140,7 @@ class PaypalGateway implements GatewayInterface public function handleWebhook(Request $request): Response { - $isSignatureValid = $this->verifyWebhookSignature($request); + $isSignatureValid = $this->paypal->verifyWebhook($request); if (!$isSignatureValid) { return new JsonResponse([ 'status' => 'ERROR', @@ -212,104 +182,6 @@ class PaypalGateway implements GatewayInterface $checkout = $this->checkoutService->chargeCheckout($checkout); } - private function verifyWebhookSignature(Request $request): bool - { - $headers = $request->headers; - - return \openssl_verify( - implode('|', [ - $headers->get('paypal-transmission-id'), - $headers->get('paypal-transmission-type'), - $this->paypalWebhookId, - \crc32($request->getContent()), - ]), - \base64_decode($headers->get('paypal-transmission-sig')), - \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) - ) === 1; - } - - public function setHttpClient(HttpClientInterface $httpClient): static - { - $this->httpClient = $httpClient; - - return $this; - } - - /** - * Calls the PayPal API to generate an OAuth2 token. - * - * @return array The API response body - */ - public function generateAuthToken(): array - { - $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ - 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], - 'body' => 'grant_type=client_credentials', - ]); - - return $response->toArray(); - } - - /** - * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. - * - * @return array The OAuth2 token data - */ - public function getAuthToken(): array - { - return $this->cache->get($this->getName(), function (ItemInterface $item): array { - $tokenData = $this->generateAuthToken(); - - $item->expiresAfter($tokenData['expires_in']); - - return $tokenData; - }); - } - - public function fetchPaypalOrder(string $orderId): array - { - $request = $this->httpClient->request( - Request::METHOD_GET, - sprintf('/v2/checkout/orders/%s', $orderId), - [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - ] - ); - - if ($request->getStatusCode() !== Response::HTTP_OK) { - throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); - } - - return \json_decode($request->getContent(), true); - } - - private function capturePaypalOrder(array $order) - { - $link = \array_filter($order['links'], function ($order) { - return $order['rel'] === 'capture'; - }); - - if (empty($link)) { - throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); - } - - $link = \array_pop($link); - $request = $this->httpClient->request( - $link['method'], - $link['href'], - [ - 'auth_bearer' => $this->getAuthToken()['access_token'], - 'headers' => ['Content-Type' => 'application/json'], - ] - ); - - if ($request->getStatusCode() !== Response::HTTP_CREATED) { - throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); - } - - return \json_decode($request->getContent(), true); - } - private function getPaypalMoney(GatewayCharge $charge): array { $brick = BrickMoney::ofMinor( From 08fdf50b48449980c5a79ee409ba2c07ac0eaa42 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:45:25 +0200 Subject: [PATCH 053/191] Update Gateways --- src/Library/Economy/Payment/PaypalGateway.php | 4 ++-- src/Library/Economy/Payment/StripeGateway.php | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 7ac7446..48b048a 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -5,10 +5,10 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; -use App\Entity\GatewayTracking; +use App\Entity\GatewayCheckoutStatus; use App\Entity\GatewayLink; use App\Entity\GatewayLinkType; -use App\Entity\GatewayCheckoutStatus; +use App\Entity\GatewayTracking; use App\Repository\GatewayCheckoutRepository; use App\Service\GatewayCheckoutService; use Brick\Money\Money as BrickMoney; diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Library/Economy/Payment/StripeGateway.php index 27da965..0874cbe 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Library/Economy/Payment/StripeGateway.php @@ -5,10 +5,10 @@ namespace App\Library\Economy\Payment; use ApiPlatform\Api\IriConverterInterface; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; -use App\Entity\GatewayTracking; +use App\Entity\GatewayCheckoutStatus; use App\Entity\GatewayLink; use App\Entity\GatewayLinkType; -use App\Entity\GatewayCheckoutStatus; +use App\Entity\GatewayTracking; use App\Repository\GatewayCheckoutRepository; use App\Service\GatewayCheckoutService; use Doctrine\ORM\EntityManagerInterface; @@ -23,6 +23,8 @@ use Symfony\Component\Routing\RouterInterface; class StripeGateway implements GatewayInterface { + public const TRACKING_TITLE_CHECKOUT = 'Stripe Checkout Session ID'; + private StripeClient $stripe; public function __construct( @@ -66,7 +68,7 @@ class StripeGateway implements GatewayInterface $tracking = new GatewayTracking(); $tracking->setValue($session->id); - $tracking->setTitle('Stripe Checkout Session ID'); + $tracking->setTitle(self::TRACKING_TITLE_CHECKOUT); $checkout->addGatewayTracking($tracking); From 3215847a347fccc57801d2c73c1603dae05de1d1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 15:04:08 +0200 Subject: [PATCH 054/191] Improve code docblocks --- src/Library/Economy/Payment/PaypalGateway.php | 14 +++----- .../Economy/Payment/PaypalGatewayService.php | 36 +++++++++++++++++-- src/State/GatewayCheckoutProcessor.php | 7 +--- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php index 48b048a..22d1236 100644 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ b/src/Library/Economy/Payment/PaypalGateway.php @@ -97,7 +97,6 @@ class PaypalGateway implements GatewayInterface $checkoutId = $request->query->get('checkoutId'); $checkout = $this->checkoutRepository->find($checkoutId); - if ($checkout === null) { throw new \Exception(sprintf("GatewayCheckout '%s' could not be found.", $checkoutId)); } @@ -107,19 +106,16 @@ class PaypalGateway implements GatewayInterface } $orderId = $request->query->get('token'); - if (!$orderId) { throw new \Exception(sprintf('PayPal checkout order ID not provided by the gateway.')); } $order = $this->paypal->getOrder($orderId); - if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) { throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId)); } - $capture = $this->paypal->captureOrder($order); - + $capture = $this->paypal->captureOrderPayment($order); if ($capture['status'] !== self::PAYPAL_STATUS_COMPLETED) { throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); } @@ -140,17 +136,17 @@ class PaypalGateway implements GatewayInterface public function handleWebhook(Request $request): Response { - $isSignatureValid = $this->paypal->verifyWebhook($request); - if (!$isSignatureValid) { + try { + $event = $this->paypal->verifyWebhook($request); + } catch (\Exception $e) { return new JsonResponse([ 'status' => 'ERROR', - 'message' => 'Could not verify webhook signature', + 'message' => $e->getMessage(), 'requestBody' => $request->getContent(), 'requestHeaders' => $request->headers->all(), ], Response::HTTP_ACCEPTED); } - $event = \json_decode($request->getContent(), true); switch ($event['event_type']) { case self::PAYPAL_EVENT_ORDER_COMPLETED: return $this->handleOrderCompleted($event); diff --git a/src/Library/Economy/Payment/PaypalGatewayService.php b/src/Library/Economy/Payment/PaypalGatewayService.php index f077531..7b04d99 100644 --- a/src/Library/Economy/Payment/PaypalGatewayService.php +++ b/src/Library/Economy/Payment/PaypalGatewayService.php @@ -67,6 +67,11 @@ class PaypalGatewayService }); } + /** + * Creates a PayPal Order resource with the given data. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ public function postOrder(array $order): array { $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ @@ -82,6 +87,11 @@ class PaypalGatewayService return $content; } + /** + * Retrieve a PayPal Order resource. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_get + */ public function getOrder(string $orderId): array { $request = $this->httpClient->request( @@ -99,7 +109,12 @@ class PaypalGatewayService return \json_decode($request->getContent(), true); } - public function captureOrder(array $order): array + /** + * Process post user-approval payment capture for a given Order. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ + public function captureOrderPayment(array $order): array { $link = \array_filter($order['links'], function ($order) { return $order['rel'] === 'capture'; @@ -126,11 +141,20 @@ class PaypalGatewayService return \json_decode($request->getContent(), true); } - public function verifyWebhook(Request $request): bool + /** + * Verifies a PayPal webhook request. + * + * @param Request $request The webhook request + * + * @return array The webhook event data + * + * @throws \Exception If the verification fails + */ + public function verifyWebhook(Request $request): array { $headers = $request->headers; - return \openssl_verify( + $isSignatureValid = \openssl_verify( implode('|', [ $headers->get('paypal-transmission-id'), $headers->get('paypal-transmission-type'), @@ -140,5 +164,11 @@ class PaypalGatewayService \base64_decode($headers->get('paypal-transmission-sig')), \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) ) === 1; + + if (!$isSignatureValid) { + throw new \Exception('Could not verify PayPal webhook signature.'); + } + + return \json_decode($request->getContent(), true); } } diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php index fef90d2..5935fe9 100644 --- a/src/State/GatewayCheckoutProcessor.php +++ b/src/State/GatewayCheckoutProcessor.php @@ -3,7 +3,6 @@ namespace App\State; use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Post; use ApiPlatform\State\ProcessorInterface; use App\Entity\GatewayCheckout; use App\Library\Economy\Payment\GatewayLocator; @@ -21,14 +20,10 @@ class GatewayCheckoutProcessor implements ProcessorInterface } /** - * @param GatewayCheckout|void $data + * @param GatewayCheckout $data */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - if (!$operation instanceof Post) { - return; - } - $checkout = $this->innerProcessor->process($data, $operation, $uriVariables, $context); $gateway = $this->gatewayLocator->getGatewayOf($checkout); From 70ac89bb3807fd1b4c85b1a7e865740985358f43 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 3 Oct 2024 13:45:35 +0200 Subject: [PATCH 055/191] Add GatewayTracking to CheckoutsPump --- src/Library/Benzina/Pump/CheckoutsPump.php | 44 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 4fc9842..f8e1c84 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -8,6 +8,7 @@ use App\Entity\GatewayCharge; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; use App\Entity\GatewayCheckoutStatus; +use App\Entity\GatewayTracking; use App\Entity\Money; use App\Entity\Tipjar; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; @@ -27,9 +28,12 @@ class CheckoutsPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; use ProgressivePumpTrait; + public const TRACKING_TITLE_V3 = 'v3 Invest ID'; + public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment'; + public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction'; + public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval'; private const PLATFORM_TIPJAR_NAME = 'platform'; - private const CHECKOUT_URL_DEFAULT = 'https://www.goteo.org/invest'; private const MAX_INT = 2147483647; @@ -115,8 +119,11 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); $checkout->setGateway($this->getCheckoutGateway($record)); - $checkout->setGatewayReference($this->getCheckoutReference($record)); - $checkout->setCheckoutUrl(self::CHECKOUT_URL_DEFAULT); + + foreach ($this->getCheckoutTrackings($record) as $tracking) { + $checkout->addGatewayTracking($tracking); + } + $checkout->setMigrated(true); $checkout->setMigratedReference($record['id']); $checkout->setMetadata([ @@ -264,20 +271,41 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } } - private function getCheckoutReference(array $record): string + /** + * @return GatewayTracking[] + */ + private function getCheckoutTrackings(array $record): array { + $v3Tracking = new GatewayTracking(); + $v3Tracking->setValue($record['id']); + $v3Tracking->setTitle(self::TRACKING_TITLE_V3); + + $trackings = [$v3Tracking]; + if (!empty($record['payment'])) { - return $record['payment']; + $payment = new GatewayTracking(); + $payment->setValue($record['payment']); + $payment->setTitle(self::TRACKING_TITLE_PAYMENT); + + $trackings[] = $payment; } if (!empty($record['transaction'])) { - return $record['transaction']; + $transaction = new GatewayTracking(); + $transaction->setValue($record['transaction']); + $transaction->setTitle(self::TRACKING_TITLE_TRANSACTION); + + $trackings[] = $transaction; } if (!empty($record['preapproval'])) { - return $record['preapproval']; + $preapproval = new GatewayTracking(); + $preapproval->setValue($record['preapproval']); + $preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL); + + $trackings[] = $preapproval; } - return ''; + return $trackings; } } From bc54d2be71bb08509daa2d0ef47c1bb07f4ad25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Tue, 8 Oct 2024 15:59:57 +0200 Subject: [PATCH 056/191] force php-cs-fixer version to 3.64 --- .github/workflows/php-cs-fixer.yaml | 2 +- .php-version | 1 + composer.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .php-version diff --git a/.github/workflows/php-cs-fixer.yaml b/.github/workflows/php-cs-fixer.yaml index 8b4ced3..1c3dea8 100644 --- a/.github/workflows/php-cs-fixer.yaml +++ b/.github/workflows/php-cs-fixer.yaml @@ -8,7 +8,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.2' - tools: php-cs-fixer + tools: php-cs-fixer:3.64 - name: Checkout repository uses: actions/checkout@v4 diff --git a/.php-version b/.php-version new file mode 100644 index 0000000..fbb9ea1 --- /dev/null +++ b/.php-version @@ -0,0 +1 @@ +8.2.0 diff --git a/composer.json b/composer.json index cf4b636..d0b66f6 100644 --- a/composer.json +++ b/composer.json @@ -90,7 +90,7 @@ } }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.58", + "friendsofphp/php-cs-fixer": "3.64", "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", From 485b639c9bcd52e4e34cf616f4de2132c534e6c9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 00:18:28 +0200 Subject: [PATCH 057/191] Update benzina pump command to show more detailed output --- src/Command/BenzinaPumpCommand.php | 112 +++++++++++++++++++---------- 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 4335d44..14ab074 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -3,12 +3,15 @@ namespace App\Command; use App\Library\Benzina\Benzina; +use App\Library\Benzina\Pump\PumpInterface; use App\Library\Benzina\Stream\PdoStream; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -28,21 +31,6 @@ class BenzinaPumpCommand extends Command { $this->addArgument('table', InputArgument::REQUIRED); - $this->addOption( - 'batch-size', - null, - InputOption::VALUE_OPTIONAL, - 'The number of rows to process at once', - 99 - ); - - $this->addOption( - 'no-progress', - null, - InputOption::VALUE_NONE, - 'For progressive pumps, disables the override of already pumped progress' - ); - $this->addOption( 'database', null, @@ -51,6 +39,14 @@ class BenzinaPumpCommand extends Command 'mysql://goteo:goteo@mariadb:3306/benzina' ); + $this->addOption( + 'batch-size', + null, + InputOption::VALUE_OPTIONAL, + 'The number of rows to process at once', + 99 + ); + $this->addUsage('app:benzina:pump --no-debug user'); $this->setHelp( <<<'EOF' @@ -65,61 +61,103 @@ EOF ); } + /** + * @param ConsoleOutputInterface $output + */ protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new SymfonyStyle($input, $output); + $batchSize = $input->getOption('batch-size'); $stream = new PdoStream( $input->getOption('database'), $input->getArgument('table'), - $input->getOption('batch-size') + $batchSize ); $streamSize = $stream->size(); + $streamBatches = $streamSize / $batchSize; + $streamSection = new SymfonyStyle($input, $output->section()); if ($streamSize < 1) { - $io->writeln('No data found at the given source. Skipping execution.'); + $streamSection->writeln('No data found at the given source. Skipping execution.'); return Command::SUCCESS; } - $pumps = $this->benzina->getPumps($stream); - $pumpsCount = count($pumps); + $streamSection->writeln(sprintf('Streaming %d records (%d batches).', $streamSize, $streamBatches)); + + $pumps = $this->benzina->getPumpsFor($stream); + $pumpsCount = \count($pumps); + $pumpsSection = new SymfonyStyle($input, $output->section()); if ($pumpsCount < 1) { - $io->writeln('No pumps available for the data. Skipping execution.'); + $pumpsSection->writeln('No pumps support the streamed sample. Skipping execution.'); return Command::SUCCESS; } - $io->writeln(sprintf('Found %d pumps.', $pumpsCount)); - $io->listing(\array_map(function ($pump) { + $pumpsSection->writeln(sprintf('Streaming to %d pumps.', $pumpsCount)); + $pumpsSection->listing(\array_map(function (PumpInterface $pump) { return $pump::class; }, $pumps)); - $io->writeln(sprintf('Processing %d records.', $streamSize)); - $progress = $io->createProgressBar(); - $progress->start($streamSize); + $etaSection = $output->section(); + $etaSection->writeln('ETA'); + + $streamedBatchesSection = $output->section(); + $streamedBatchesSection->writeln("\tStreamed batches:"); + $streamedBatches = new ProgressBar($streamedBatchesSection); + $streamedBatches->start($streamBatches); + + $streamedRecordsSection = $output->section(); + $streamedRecordsSection->writeln("\tStreamed records:"); + $streamedRecords = new ProgressBar($streamedRecordsSection); + $streamedRecords->start($streamSize); + + $memUsageSection = $output->section(); + $memUsageSection->writeln("\tMemory usage:"); + $memUsage = new ProgressBar($memUsageSection); + $memUsage->start($this->getMemoryLimit()); while (!$stream->eof()) { - $data = $stream->read(); + \memory_reset_peak_usage(); + + $batch = $stream->read(); + $batchStartTime = \microtime(true); + $streamedBatches->advance(); foreach ($pumps as $pump) { - $pump->configure([ - 'progress' => !$input->getOption('no-progress'), - ]); + $pump->pump($batch); - $pump->process($data); + $memUsage->setProgress(\memory_get_peak_usage(false)); } - $progress->setProgress($stream->tell()); + $streamed = $stream->tell(); + $streamedRecords->setProgress($streamed); + + $batchEndTime = \microtime(true); + $batchTime = $batchEndTime - $batchStartTime; + + $eta = round((($streamSize - $streamed) / count($batch)) * $batchTime); + $etaSection->overwrite(sprintf('ETA %02d:%02d:%02d', $eta / 3600, floor($eta / 60) % 60, $eta % 60)); } - $stream->close(); - $progress->finish(); - - $io->writeln("\n"); - $io->success('Data processed successfully!'); + $endSection = new SymfonyStyle($input, $output->section()); + $endSection->success('Data processed successfully!'); return Command::SUCCESS; } + + private function getMemoryLimit(): int + { + $limit = \ini_get('memory_limit'); + if (preg_match('/^(\d+)(.)$/', $limit, $matches)) { + if ($matches[2] == 'M') { + $limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB + } else if ($matches[2] == 'K') { + $limit = $matches[1] * 1024; // nnnK -> nnn KB + } + } + + return $limit; + } } From 22314b85dcf73777c563ac4d50e5cdb15f7feda1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 00:20:50 +0200 Subject: [PATCH 058/191] Rename pump interface main process method --- src/Library/Benzina/Benzina.php | 2 +- src/Library/Benzina/Pump/CheckoutsPump.php | 32 +++++++++++----------- src/Library/Benzina/Pump/ProjectsPump.php | 25 ++++++++--------- src/Library/Benzina/Pump/PumpInterface.php | 20 +++++++------- src/Library/Benzina/Pump/UsersPump.php | 21 +++++++------- 5 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/Library/Benzina/Benzina.php b/src/Library/Benzina/Benzina.php index a958519..99fdede 100644 --- a/src/Library/Benzina/Benzina.php +++ b/src/Library/Benzina/Benzina.php @@ -23,7 +23,7 @@ class Benzina * * @return PumpInterface[] */ - public function getPumps(StreamInterface $stream, int $sampleSize = 1): array + public function getPumpsFor(StreamInterface $stream, int $sampleSize = 1): array { $sample = $stream->read($sampleSize); $stream->rewind(); diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index f8e1c84..8f03996 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -77,25 +77,25 @@ class CheckoutsPump extends AbstractPump implements PumpInterface ) { } - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\array_key_exists(0, $data)) { + if (!\is_array($batch) || !\array_key_exists(0, $batch)) { return false; } - return $this->hasAllKeys($data[0], self::INVEST_KEYS); + return $this->hasAllKeys($batch[0], self::INVEST_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $users = $this->getUsers($data); - $projects = $this->getProjects($data); + $users = $this->getUsers($batch); + $projects = $this->getProjects($batch); $tipjar = $this->getPlatformTipjar(); - $pumped = $this->getPumped(GatewayCheckout::class, $data, ['migratedReference' => 'id']); + $pumped = $this->getPumped(GatewayCheckout::class, $batch, ['migratedReference' => 'id']); - foreach ($data as $key => $record) { + foreach ($batch as $key => $record) { if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { continue; } @@ -173,11 +173,11 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $this->entityManager->clear(); } - private function getUsers(array $data): array + private function getUsers(array $batch): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['user']; - }, $data)]); + $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($batch) { + return $batch['user']; + }, $batch)]); $usersByMigratedReference = []; foreach ($users as $user) { @@ -187,11 +187,11 @@ class CheckoutsPump extends AbstractPump implements PumpInterface return $usersByMigratedReference; } - private function getProjects(array $data): array + private function getProjects(array $batch): array { - $projects = $this->projectRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['project']; - }, $data)]); + $projects = $this->projectRepository->findBy(['migratedReference' => \array_map(function ($batch) { + return $batch['project']; + }, $batch)]); $projectsByMigratedReference = []; foreach ($projects as $project) { diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index 859f2b3..b6bd89d 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -97,24 +97,23 @@ class ProjectsPump extends AbstractPump implements PumpInterface public function __construct( private UserRepository $userRepository, private EntityManagerInterface $entityManager, - ) { - } + ) {} - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\array_key_exists(0, $data)) { + if (!\is_array($batch) || !\array_key_exists(0, $batch)) { return false; } - return $this->hasAllKeys($data[0], self::PROJECT_KEYS); + return $this->hasAllKeys($batch[0], self::PROJECT_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $pumped = $this->getPumped(Project::class, $data, ['migratedReference' => 'id']); - $owners = $this->getOwners($data); + $pumped = $this->getPumped(Project::class, $batch, ['migratedReference' => 'id']); + $owners = $this->getOwners($batch); - foreach ($data as $key => $record) { + foreach ($batch as $key => $record) { $isPumped = $this->isPumped($record, $pumped, ['migratedReference' => 'id']); if ($isPumped) { @@ -151,11 +150,11 @@ class ProjectsPump extends AbstractPump implements PumpInterface /** * @return User[] */ - private function getOwners(array $data): array + private function getOwners(array $record): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['owner']; - }, $data)]); + $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($record) { + return $record['owner']; + }, $record)]); $owners = []; foreach ($users as $user) { diff --git a/src/Library/Benzina/Pump/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index f071335..0bec9af 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -4,13 +4,6 @@ namespace App\Library\Benzina\Pump; interface PumpInterface { - /** - * Determines if the data is supported by this pump. - * - * @param mixed $data The streamed records, e.g. rows from an user table - */ - public function supports(mixed $data): bool; - /** * Sets flexible configuration values for this pump. * @@ -19,9 +12,16 @@ interface PumpInterface public function configure(array $config = []): void; /** - * Process the data to be pumped. + * Determines if a data batch is supported by this pump. * - * @param mixed $data The streamed records, e.g. rows from an user table + * @param mixed $batch A sample of the streamed records, e.g. rows from an user table */ - public function process(mixed $data): void; + public function supports(mixed $batch): bool; + + /** + * Pump a data batch into a final destination. + * + * @param mixed $batch The streamed records, e.g. rows from an user table + */ + public function pump(mixed $batch): void; } diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 15848bd..e6a1126 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -52,23 +52,22 @@ class UsersPump extends AbstractPump implements PumpInterface public function __construct( private EntityManagerInterface $entityManager, - ) { - } + ) {} - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\is_array($data[0])) { + if (!\is_array($batch) || !\is_array($batch[0])) { return false; } - return $this->hasAllKeys($data[0], self::USER_KEYS); + return $this->hasAllKeys($batch[0], self::USER_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $pumped = $this->getPumped(User::class, $data, ['migratedReference' => 'id']); + $pumped = $this->getPumped(User::class, $batch, ['migratedReference' => 'id']); - foreach ($data as $key => $record) { + foreach ($batch as $key => $record) { if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { continue; } @@ -113,12 +112,12 @@ class UsersPump extends AbstractPump implements PumpInterface return $username; } - private function getUsername(array $data): string + private function getUsername(array $record): string { - $username = $this->normalizeUsername($data['id']); + $username = $this->normalizeUsername($record['id']); if (!$username) { - $username = $this->normalizeUsername($data['email']); + $username = $this->normalizeUsername($record['email']); } return $username; From 1661f839f21efb40a59f5b723b009de91cbd91bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Wed, 9 Oct 2024 11:59:40 +0200 Subject: [PATCH 059/191] remove .php-version file --- .php-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .php-version diff --git a/.php-version b/.php-version deleted file mode 100644 index fbb9ea1..0000000 --- a/.php-version +++ /dev/null @@ -1 +0,0 @@ -8.2.0 From 73a2fbc1ca8018d03bb32933527d0de8cf33b0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ig=C3=B3n?= Date: Wed, 9 Oct 2024 12:00:52 +0200 Subject: [PATCH 060/191] add .php-version to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 23fbd46..5e361fb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ ###> doctrineencryptbundle/doctrine-encrypt-bundle .Halite.key ###< doctrineencryptbundle/doctrine-encrypt-bundle + +.php-version From 6ca8ce184c0ab7b9baa0ee097e715cf5ddfe1841 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 13:36:07 +0200 Subject: [PATCH 061/191] Reduce progressive pump trait memory usage --- .../Benzina/Pump/Trait/DoctrinePumpTrait.php | 54 +++++++++ .../Pump/Trait/ProgressivePumpTrait.php | 103 ------------------ 2 files changed, 54 insertions(+), 103 deletions(-) create mode 100644 src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php delete mode 100644 src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php new file mode 100644 index 0000000..1dcf486 --- /dev/null +++ b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php @@ -0,0 +1,54 @@ +getConfig('skip-pumped')) { + return $batch; + } + + $repository = $this->entityManager->getRepository($entityClass); + + $pumped = $repository->findBy([$entityKey => \array_map(function ($record) use ($batchKey) { + return $record[$batchKey]; + }, $batch)]); + + $pumpedKeys = []; + foreach ($pumped as $key => $entity) { + $entityKeyGetter = sprintf('get%s', \ucfirst($entityKey)); + + $pumpedKeys[] = $entity->$entityKeyGetter(); + } + + return \array_filter($batch, function (array $record) use ($batchKey, $pumpedKeys) { + return !\in_array($record[$batchKey], $pumpedKeys); + }); + } +} diff --git a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php b/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php deleted file mode 100644 index 58cb192..0000000 --- a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php +++ /dev/null @@ -1,103 +0,0 @@ -getPumped(Entity::class, $data, ['entityId' => 'dataId']); - * ... - * if ($this->isPumped($record, $alreadyPumped)) continue; - * ``` - */ -trait ProgressivePumpTrait -{ - public function __construct( - private EntityManagerInterface $entityManager, - ) { - } - - /** - * Retrieve already pumped data from the pumping data batch. - * - * @param string $entityClass The class to pump the data to - * @param array $pumpingBatch The pumping data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function getPumped( - string $entityClass, - array $pumpingBatch, - array $matchCriteria, - ): array { - $repository = $this->entityManager->getRepository($entityClass); - - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - $pumped = $repository->findBy([$entityKey => \array_map(function ($data) use ($pumpingKey) { - return $data[$pumpingKey]; - }, $pumpingBatch)]); - - $pumpedByKey = []; - foreach ($pumped as $pumped) { - $getter = sprintf('get%s', ucfirst($entityKey)); - $pumpedByKey[$pumped->$getter()] = $pumped; - } - - return $pumpedByKey; - } - - /** - * Determine if a pumping data record is already in a pumped data batch. - * - * @param array $pumpingRecord The data record to be pumped - * @param array $pumpedBatch The pumped data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function isPumped( - array $pumpingRecord, - array $pumpedBatch, - array $matchCriteria, - ): bool { - if ( - isset($this->config) - && $this->config['progress'] === false - && \array_key_exists('progress', $this->config) - ) { - return false; - } - - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - return \array_key_exists($pumpingRecord[$pumpingKey], $pumpedBatch); - } - - /** - * Obtain the pumped record in a pumped data batch from a pumping data record. - * - * @param array $pumpingRecord The data record being pumped - * @param array $pumpedBatch The pumped data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function getPumpedRecord( - array $pumpingRecord, - array $pumpedBatch, - array $matchCriteria, - ): mixed { - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - if ($this->isPumped($pumpingRecord, $pumpedBatch, $matchCriteria)) { - return $pumpedBatch[$pumpingRecord[$pumpingKey]]; - } - - return false; - } -} From 8402d33ecc920ddf1975cb881d365c507d341a52 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 13:36:40 +0200 Subject: [PATCH 062/191] Update pump config write and read interface --- src/Library/Benzina/Pump/AbstractPump.php | 13 +++++++++++-- src/Library/Benzina/Pump/PumpInterface.php | 12 ++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Library/Benzina/Pump/AbstractPump.php b/src/Library/Benzina/Pump/AbstractPump.php index b349421..f0d09f4 100644 --- a/src/Library/Benzina/Pump/AbstractPump.php +++ b/src/Library/Benzina/Pump/AbstractPump.php @@ -4,10 +4,19 @@ namespace App\Library\Benzina\Pump; abstract class AbstractPump implements PumpInterface { - protected array $config; + protected array $config = []; - public function configure(array $config = []): void + public function setConfig(array $config = []): void { $this->config = $config; } + + public function getConfig(?string $key = null): array + { + if ($key !== null) { + return [$key => $this->config[$key]]; + } + + return $this->config; + } } diff --git a/src/Library/Benzina/Pump/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index 0bec9af..9af9455 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -7,9 +7,17 @@ interface PumpInterface /** * Sets flexible configuration values for this pump. * - * @param array $config the configuration array + * @param array $config The configuration array */ - public function configure(array $config = []): void; + public function setConfig(array $config = []): void; + + /** + * Read the configuration values for this pump. + * + * @param string|null $key A configuration array key to return + * @return array The configuration array at the specified key, or all keys if null + */ + public function getConfig(?string $key = null): array; /** * Determines if a data batch is supported by this pump. From 8659ab0522af9f097ca9b982c82ae67edf2791e9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 13:37:02 +0200 Subject: [PATCH 063/191] Configure pumps on pump command --- src/Command/BenzinaPumpCommand.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 14ab074..171101e 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -47,6 +47,14 @@ class BenzinaPumpCommand extends Command 99 ); + $this->addOption( + 'skip-pumped', + null, + InputOption::VALUE_OPTIONAL, + 'Skips feeding already pumped records in a batch', + true + ); + $this->addUsage('app:benzina:pump --no-debug user'); $this->setHelp( <<<'EOF' @@ -123,9 +131,15 @@ EOF $batch = $stream->read(); $batchStartTime = \microtime(true); + + $streamBatches = $streamBatches - 1; $streamedBatches->advance(); - foreach ($pumps as $pump) { + foreach ($pumps as $key => $pump) { + $pump->setConfig([ + 'skip-pumped' => $input->getOption('skip-pumped') + ]); + $pump->pump($batch); $memUsage->setProgress(\memory_get_peak_usage(false)); @@ -135,9 +149,9 @@ EOF $streamedRecords->setProgress($streamed); $batchEndTime = \microtime(true); - $batchTime = $batchEndTime - $batchStartTime; + $batchRunTime = $batchEndTime - $batchStartTime; - $eta = round((($streamSize - $streamed) / count($batch)) * $batchTime); + $eta = round($streamBatches * $batchRunTime); $etaSection->overwrite(sprintf('ETA %02d:%02d:%02d', $eta / 3600, floor($eta / 60) % 60, $eta % 60)); } From d8624476a574040872535e3ae3d9222174f7841b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 14:30:54 +0200 Subject: [PATCH 064/191] Make GatewayCharge api resource --- src/Entity/GatewayCharge.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php index 22ff38d..c78735f 100644 --- a/src/Entity/GatewayCharge.php +++ b/src/Entity/GatewayCharge.php @@ -10,6 +10,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\ApiResource()] #[ORM\Entity(repositoryClass: GatewayChargeRepository::class)] class GatewayCharge { From 1cf4425855f4bfa6e5f778a415b9a20a4324f02a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 15:36:18 +0200 Subject: [PATCH 065/191] Add ORM Index to migrated reference fields --- src/Entity/GatewayCheckout.php | 1 + src/Entity/User.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index c97afc3..38e9a30 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -27,6 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\Get()] #[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])] #[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)] +#[ORM\Index(fields: ['migratedReference'])] class GatewayCheckout { use TimestampableCreationEntity; diff --git a/src/Entity/User.php b/src/Entity/User.php index 3b309d1..dea66da 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -37,6 +37,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] +#[ORM\Index(fields: ['migratedReference'])] class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface { use TimestampableCreationEntity; From a2eaf8d92d33106c9441801a51cc90c63d55e94a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 15:41:22 +0200 Subject: [PATCH 066/191] Rename migratedReference to migratedId --- src/Entity/GatewayCheckout.php | 12 ++++++------ src/Entity/Project.php | 10 +++++----- src/Entity/User.php | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 38e9a30..23699b2 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -27,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\Get()] #[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])] #[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)] -#[ORM\Index(fields: ['migratedReference'])] +#[ORM\Index(fields: ['migratedId'])] class GatewayCheckout { use TimestampableCreationEntity; @@ -104,7 +104,7 @@ class GatewayCheckout */ #[API\ApiProperty(writable: false)] #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedReference = null; + private ?string $migratedId = null; /** * A free-form collection of additional data associated with this checkout operation. @@ -257,14 +257,14 @@ class GatewayCheckout return $this; } - public function getMigratedReference(): ?string + public function getMigratedId(): ?string { - return $this->migratedReference; + return $this->migratedId; } - public function setMigratedReference(?string $migratedReference): static + public function setMigratedId(?string $migratedId): static { - $this->migratedReference = $migratedReference; + $this->migratedId = $migratedId; return $this; } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 35e01a6..09fb57d 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -76,7 +76,7 @@ class Project */ #[API\ApiProperty(writable: false)] #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedReference = null; + private ?string $migratedId = null; public function __construct() { @@ -148,14 +148,14 @@ class Project return $this; } - public function getMigratedReference(): ?string + public function getMigratedId(): ?string { - return $this->migratedReference; + return $this->migratedId; } - public function setMigratedReference(?string $migratedReference): static + public function setMigratedId(?string $migratedId): static { - $this->migratedReference = $migratedReference; + $this->migratedId = $migratedId; return $this; } diff --git a/src/Entity/User.php b/src/Entity/User.php index dea66da..078978b 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -37,7 +37,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] -#[ORM\Index(fields: ['migratedReference'])] +#[ORM\Index(fields: ['migratedId'])] class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface { use TimestampableCreationEntity; @@ -137,7 +137,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs */ #[API\ApiProperty(writable: false)] #[ORM\Column(length: 255, nullable: true)] - private ?string $migratedReference = null; + private ?string $migratedId = null; /** * The projects owned by this User. @@ -374,14 +374,14 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs return $this; } - public function getMigratedReference(): ?string + public function getMigratedId(): ?string { - return $this->migratedReference; + return $this->migratedId; } - public function setMigratedReference(?string $migratedReference): static + public function setMigratedId(?string $migratedId): static { - $this->migratedReference = $migratedReference; + $this->migratedId = $migratedId; return $this; } From b7ad6ed7bd3060061c7d1220924574aceb95dd6c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 15:43:05 +0200 Subject: [PATCH 067/191] Update DoctrinePumpTrait --- .../Benzina/Pump/Trait/DoctrinePumpTrait.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php index 1dcf486..453b346 100644 --- a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php +++ b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php @@ -9,7 +9,11 @@ trait DoctrinePumpTrait { public function __construct( private EntityManagerInterface $entityManager, - ) {} + ) { + if (!$this instanceof PumpInterface) { + throw new \Exception("DoctrinePumpTrait can only be used with PumpInterface classes"); + } + } /** * Skip already pumped records from a pumping stream batch. @@ -26,19 +30,15 @@ trait DoctrinePumpTrait string $entityClass, string $entityKey, ): array { - if (!$this instanceof PumpInterface) { - throw new \Exception("DoctrinePumpTrait can only be used with PumpInterface classes"); - } - if (!$this->getConfig('skip-pumped')) { return $batch; } - $repository = $this->entityManager->getRepository($entityClass); - - $pumped = $repository->findBy([$entityKey => \array_map(function ($record) use ($batchKey) { - return $record[$batchKey]; - }, $batch)]); + $pumped = $this->entityManager + ->getRepository($entityClass) + ->findBy([$entityKey => \array_map(function (array $record) use ($batchKey) { + return $record[$batchKey]; + }, $batch)]); $pumpedKeys = []; foreach ($pumped as $key => $entity) { From d6abe24cbc8cc220cb91a1b20b39d63aa0e70eb8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 15:45:41 +0200 Subject: [PATCH 068/191] Update pumps to new interface --- src/Library/Benzina/Pump/CheckoutsPump.php | 58 ++++++++++++---------- src/Library/Benzina/Pump/ProjectsPump.php | 19 +++---- src/Library/Benzina/Pump/UsersPump.php | 12 ++--- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 8f03996..11c9a4f 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -12,7 +12,7 @@ use App\Entity\GatewayTracking; use App\Entity\Money; use App\Entity\Tipjar; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; -use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait; +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; @@ -27,7 +27,8 @@ use Doctrine\ORM\EntityManagerInterface; class CheckoutsPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; + public const TRACKING_TITLE_V3 = 'v3 Invest ID'; public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment'; public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction'; @@ -74,8 +75,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, private EntityManagerInterface $entityManager, - ) { - } + ) {} public function supports(mixed $batch): bool { @@ -88,19 +88,15 @@ class CheckoutsPump extends AbstractPump implements PumpInterface public function pump(mixed $batch): void { - $users = $this->getUsers($batch); - $projects = $this->getProjects($batch); + $batch = $this->skipPumped($batch, 'id', GatewayCheckout::class, 'migratedId'); $tipjar = $this->getPlatformTipjar(); - $pumped = $this->getPumped(GatewayCheckout::class, $batch, ['migratedReference' => 'id']); + $users = $this->getPumpedUsers($batch); + $projects = $this->getPumpedProjects($batch); foreach ($batch as $key => $record) { - if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { - continue; - } - - if (!\array_key_exists($record['project'], $projects)) { + if (!$record['user'] || empty($record['user'])) { continue; } @@ -113,7 +109,6 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } $user = $users[$record['user']]; - $project = $projects[$record['project']]; $checkout = new GatewayCheckout(); $checkout->setOrigin($user->getAccounting()); @@ -125,7 +120,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } $checkout->setMigrated(true); - $checkout->setMigratedReference($record['id']); + $checkout->setMigratedId($record['id']); $checkout->setMetadata([ 'payment' => $record['payment'], 'transaction' => $record['transaction'], @@ -133,11 +128,21 @@ class CheckoutsPump extends AbstractPump implements PumpInterface ]); $checkout->setDateCreated(new \DateTime($record['invested'])); + $checkout->setDateUpdated(new \DateTime()); $charge = new GatewayCharge(); $charge->setType($this->getChargeType($record)); $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); - $charge->setTarget($project->getAccounting()); + + if (empty($record['project'])) { + $charge->setTarget($user->getAccounting()); + } + + if (!empty($record['project'])) { + $project = $projects[$record['project']]; + + $charge->setTarget($project->getAccounting()); + } if ($record['donate_amount'] > 0) { $tip = new GatewayCharge(); @@ -145,11 +150,9 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency'])); $tip->setTarget($tipjar->getAccounting()); - $this->entityManager->persist($tip); $checkout->addCharge($tip); } - $this->entityManager->persist($charge); $checkout->addCharge($charge); if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { @@ -160,9 +163,6 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $transaction->setTarget($charge->getTarget()); $charge->setTransaction($transaction); - - $this->entityManager->persist($transaction); - $this->entityManager->persist($charge); } } @@ -173,29 +173,35 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $this->entityManager->clear(); } - private function getUsers(array $batch): array + /** + * @return array + */ + private function getPumpedUsers(array $batch): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($batch) { + $users = $this->userRepository->findBy(['migratedId' => \array_map(function ($batch) { return $batch['user']; }, $batch)]); $usersByMigratedReference = []; foreach ($users as $user) { - $usersByMigratedReference[$user->getMigratedReference()] = $user; + $usersByMigratedReference[$user->getMigratedId()] = $user; } return $usersByMigratedReference; } - private function getProjects(array $batch): array + /** + * @return array + */ + private function getPumpedProjects(array $batch): array { - $projects = $this->projectRepository->findBy(['migratedReference' => \array_map(function ($batch) { + $projects = $this->projectRepository->findBy(['migratedId' => \array_map(function ($batch) { return $batch['project']; }, $batch)]); $projectsByMigratedReference = []; foreach ($projects as $project) { - $projectsByMigratedReference[$project->getMigratedReference()] = $project; + $projectsByMigratedReference[$project->getMigratedId()] = $project; } return $projectsByMigratedReference; diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index b6bd89d..515a956 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -7,14 +7,14 @@ use App\Entity\Project; use App\Entity\ProjectStatus; use App\Entity\User; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; -use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait; +use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; class ProjectsPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; private const PROJECT_KEYS = [ 'id', @@ -110,16 +110,11 @@ class ProjectsPump extends AbstractPump implements PumpInterface public function pump(mixed $batch): void { - $pumped = $this->getPumped(Project::class, $batch, ['migratedReference' => 'id']); + $batch = $this->skipPumped($batch, 'id', Project::class, 'migratedId'); + $owners = $this->getOwners($batch); foreach ($batch as $key => $record) { - $isPumped = $this->isPumped($record, $pumped, ['migratedReference' => 'id']); - - if ($isPumped) { - continue; - } - if (!\array_key_exists($record['owner'], $owners)) { continue; } @@ -133,7 +128,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface $project->setOwner($owners[$record['owner']]); $project->setStatus($this->getProjectStatus($record['status'])); $project->setMigrated(true); - $project->setMigratedReference($record['id']); + $project->setMigratedId($record['id']); $project->setDateCreated(new \DateTime($record['created'])); $accounting = $this->getAccounting($record); @@ -152,13 +147,13 @@ class ProjectsPump extends AbstractPump implements PumpInterface */ private function getOwners(array $record): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($record) { + $users = $this->userRepository->findBy(['migratedId' => \array_map(function ($record) { return $record['owner']; }, $record)]); $owners = []; foreach ($users as $user) { - $owners[$user->getMigratedReference()] = $user; + $owners[$user->getMigratedId()] = $user; } return $owners; diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index e6a1126..9e42a6b 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -5,13 +5,13 @@ 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\ProgressivePumpTrait; +use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; use Doctrine\ORM\EntityManagerInterface; class UsersPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; public const USER_KEYS = [ 'id', @@ -65,13 +65,9 @@ class UsersPump extends AbstractPump implements PumpInterface public function pump(mixed $batch): void { - $pumped = $this->getPumped(User::class, $batch, ['migratedReference' => 'id']); + $batch = $this->skipPumped($batch, 'id', User::class, 'migratedId'); foreach ($batch as $key => $record) { - if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { - continue; - } - $user = new User(); $user->setAccounting(new Accounting()); $user->setUsername($this->getUsername($record)); @@ -81,7 +77,7 @@ class UsersPump extends AbstractPump implements PumpInterface $user->setName($record['name']); $user->setActive(false); $user->setMigrated(true); - $user->setMigratedReference($record['id']); + $user->setMigratedId($record['id']); $accounting = new Accounting(); $accounting->setUser($user); From 2fa4b3bcacbcd8e2eac5e517d7da7747ac4f1bf8 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 9 Oct 2024 15:52:13 +0200 Subject: [PATCH 069/191] Code style fixes --- src/Command/BenzinaPumpCommand.php | 4 ++-- src/Library/Benzina/Pump/CheckoutsPump.php | 3 ++- src/Library/Benzina/Pump/ProjectsPump.php | 3 ++- src/Library/Benzina/Pump/PumpInterface.php | 3 ++- src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php | 11 ++++++----- src/Library/Benzina/Pump/UsersPump.php | 3 ++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 171101e..9fe923b 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -137,7 +137,7 @@ EOF foreach ($pumps as $key => $pump) { $pump->setConfig([ - 'skip-pumped' => $input->getOption('skip-pumped') + 'skip-pumped' => $input->getOption('skip-pumped'), ]); $pump->pump($batch); @@ -167,7 +167,7 @@ EOF if (preg_match('/^(\d+)(.)$/', $limit, $matches)) { if ($matches[2] == 'M') { $limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB - } else if ($matches[2] == 'K') { + } elseif ($matches[2] == 'K') { $limit = $matches[1] * 1024; // nnnK -> nnn KB } } diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 11c9a4f..e4c3632 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -75,7 +75,8 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, private EntityManagerInterface $entityManager, - ) {} + ) { + } public function supports(mixed $batch): bool { diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index 515a956..f6cb1f0 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -97,7 +97,8 @@ 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/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index 9af9455..163ee88 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -13,8 +13,9 @@ interface PumpInterface /** * Read the configuration values for this pump. - * + * * @param string|null $key A configuration array key to return + * * @return array The configuration array at the specified key, or all keys if null */ public function getConfig(?string $key = null): array; diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php index 453b346..61e2e78 100644 --- a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php +++ b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php @@ -11,17 +11,18 @@ trait DoctrinePumpTrait private EntityManagerInterface $entityManager, ) { if (!$this instanceof PumpInterface) { - throw new \Exception("DoctrinePumpTrait can only be used with PumpInterface classes"); + throw new \Exception('DoctrinePumpTrait can only be used with PumpInterface classes'); } } /** * Skip already pumped records from a pumping stream batch. * - * @param array $batch The batch of records being pumped - * @param array $batchKey The key in the batch records to match against the entity key - * @param string $entityClass The class where the data is being pumped to - * @param string $entityKey The property of the pumped to class to match against the batch key + * @param array $batch The batch of records being pumped + * @param array $batchKey The key in the batch records to match against the entity key + * @param string $entityClass The class where the data is being pumped to + * @param string $entityKey The property of the pumped to class to match against the batch key + * * @return array The batch of records being pumped, sliced out of already pumped records */ public function skipPumped( diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 9e42a6b..14485b5 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -52,7 +52,8 @@ class UsersPump extends AbstractPump implements PumpInterface public function __construct( private EntityManagerInterface $entityManager, - ) {} + ) { + } public function supports(mixed $batch): bool { From ed138941a6734a31650d4f9e1c9f0d072346c784 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 12:31:36 +0200 Subject: [PATCH 070/191] Allow to return default value for missing pump config keys --- src/Library/Benzina/Pump/AbstractPump.php | 8 ++++++-- src/Library/Benzina/Pump/PumpInterface.php | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Library/Benzina/Pump/AbstractPump.php b/src/Library/Benzina/Pump/AbstractPump.php index f0d09f4..b89810a 100644 --- a/src/Library/Benzina/Pump/AbstractPump.php +++ b/src/Library/Benzina/Pump/AbstractPump.php @@ -11,10 +11,14 @@ abstract class AbstractPump implements PumpInterface $this->config = $config; } - public function getConfig(?string $key = null): array + public function getConfig(?string $key = null, mixed $default = null): array { if ($key !== null) { - return [$key => $this->config[$key]]; + return [ + $key => \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 163ee88..043402e 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -15,10 +15,11 @@ interface PumpInterface * Read the configuration values for this pump. * * @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 */ - public function getConfig(?string $key = null): array; + public function getConfig(?string $key = null, mixed $default = null): array; /** * Determines if a data batch is supported by this pump. From b9512557996a450268b7b47df7c4076a616c0deb Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 12:32:14 +0200 Subject: [PATCH 071/191] No skip pumped data by default in DoctrinePumpTrait --- src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php index 61e2e78..f7a86a7 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')) { + if (!$this->getConfig('skip-pumped', false)) { return $batch; } From fb94af2d4834ed11b3d7f923bd03ee251ce60b43 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 12:32:49 +0200 Subject: [PATCH 072/191] Fix code usage in UsersPumpTest --- tests/Library/Benzina/Pump/UsersPumpTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Library/Benzina/Pump/UsersPumpTest.php b/tests/Library/Benzina/Pump/UsersPumpTest.php index 6b7d32e..b4afbb8 100644 --- a/tests/Library/Benzina/Pump/UsersPumpTest.php +++ b/tests/Library/Benzina/Pump/UsersPumpTest.php @@ -95,7 +95,7 @@ class UsersPumpTest extends KernelTestCase $this->assertCount(0, $usersPrePumping); - $this->pump->process([$testUser]); + $this->pump->pump([$testUser]); $usersPostPumping = $this->entityManager->getRepository(User::class) ->findAll(); @@ -108,6 +108,6 @@ class UsersPumpTest extends KernelTestCase $this->assertNotEquals($testUser['confirmed'], $user->isEmailConfirmed()); $this->assertTrue($user->isMigrated()); - $this->assertEquals($testUser['id'], $user->getMigratedReference()); + $this->assertEquals($testUser['id'], $user->getMigratedId()); } } From 45f35d823b6299d058fa9babcf21758f01233055 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 12:39:27 +0200 Subject: [PATCH 073/191] Rename pumped databy migrated key var names --- src/Library/Benzina/Pump/CheckoutsPump.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index e4c3632..d52fc9d 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -183,12 +183,12 @@ class CheckoutsPump extends AbstractPump implements PumpInterface return $batch['user']; }, $batch)]); - $usersByMigratedReference = []; + $usersByMigratedId = []; foreach ($users as $user) { - $usersByMigratedReference[$user->getMigratedId()] = $user; + $usersByMigratedId[$user->getMigratedId()] = $user; } - return $usersByMigratedReference; + return $usersByMigratedId; } /** @@ -200,12 +200,12 @@ class CheckoutsPump extends AbstractPump implements PumpInterface return $batch['project']; }, $batch)]); - $projectsByMigratedReference = []; + $projectsByMigratedId = []; foreach ($projects as $project) { - $projectsByMigratedReference[$project->getMigratedId()] = $project; + $projectsByMigratedId[$project->getMigratedId()] = $project; } - return $projectsByMigratedReference; + return $projectsByMigratedId; } private function getPlatformTipjar(): Tipjar From 9371e20a4ebf510739c101c43ecab0fa48c5d665 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 12:39:46 +0200 Subject: [PATCH 074/191] Default to no skipping pumped data in pump command --- 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 9fe923b..40b5016 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -52,7 +52,7 @@ class BenzinaPumpCommand extends Command null, InputOption::VALUE_OPTIONAL, 'Skips feeding already pumped records in a batch', - true + false ); $this->addUsage('app:benzina:pump --no-debug user'); From 58dc912e3a48eb317439c52947cb7949661ed294 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 13:22:26 +0200 Subject: [PATCH 075/191] Add date updated to projects in projects pump --- src/Library/Benzina/Pump/ProjectsPump.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index f6cb1f0..a93870c 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -131,6 +131,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface $project->setMigrated(true); $project->setMigratedId($record['id']); $project->setDateCreated(new \DateTime($record['created'])); + $project->setDateUpdated(new \DateTime()); $accounting = $this->getAccounting($record); $accounting->setProject($project); From e047537e457a89e78c4b55ff570c49ce6a03e899 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 14:09:57 +0200 Subject: [PATCH 076/191] GatewayCheckout links and trackings dont need to be a collection --- src/Entity/GatewayCheckout.php | 59 ++++++------- src/Entity/GatewayLink.php | 93 +------------------- src/Entity/GatewayTracking.php | 68 ++------------ src/Repository/GatewayLinkRepository.php | 43 --------- src/Repository/GatewayTrackingRepository.php | 43 --------- 5 files changed, 38 insertions(+), 268 deletions(-) delete mode 100644 src/Repository/GatewayLinkRepository.php delete mode 100644 src/Repository/GatewayTrackingRepository.php diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index 23699b2..f639d54 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -76,21 +76,21 @@ class GatewayCheckout * A list of URLs provided by the Gateway for this checkout.\ * e.g: Fulfill payment, API resource address. * - * @var Collection + * @var GatewayLink[] */ #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayLink::class, cascade: ['persist'])] - private Collection $gatewayLinks; + #[ORM\Column] + private array $gatewayLinks = []; /** * A list of tracking codes provided by the Gateway for this checkout.\ * e.g: Order ID, Payment Capture ID, Checkout Session Token. - * - * @var Collection + * + * @var GatewayTracking[] */ #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayTracking::class, cascade: ['persist'])] - private Collection $gatewayTrackings; + #[ORM\Column] + private array $gatewayTrackings = []; /** * GatewayCheckout was migrated from an invest record in Goteo v3 platform. @@ -116,8 +116,6 @@ class GatewayCheckout { $this->charges = new ArrayCollection(); $this->status = GatewayCheckoutStatus::Pending; - $this->gatewayLinks = new ArrayCollection(); - $this->gatewayTrackings = new ArrayCollection(); } public function getId(): ?int @@ -186,61 +184,56 @@ class GatewayCheckout } /** - * @return Collection + * @return GatewayLink[] */ - public function getGatewayLinks(): Collection + public function getGatewayLinks(): array { return $this->gatewayLinks; } public function addGatewayLink(GatewayLink $link): static { - if (!$this->gatewayLinks->contains($link)) { - $this->gatewayLinks->add($link); - $link->setCheckout($this); - } + $this->gatewayLinks = [...$this->gatewayLinks, $link]; return $this; } public function removeGatewayLink(GatewayLink $link): static { - if ($this->gatewayLinks->removeElement($link)) { - // set the owning side to null (unless already changed) - if ($link->getCheckout() === $this) { - $link->setCheckout(null); + $this->gatewayLinks = \array_filter( + $this->gatewayLinks, + function (GatewayLink $existingLink) use ($link) { + return $existingLink->href !== $link->href; } - } + ); return $this; } /** - * @return Collection + * @return GatewayTracking[] */ - public function getGatewayTrackings(): Collection + public function getGatewayTrackings(): array { return $this->gatewayTrackings; } - public function addGatewayTracking(GatewayTracking $gatewayTracking): static + public function addGatewayTracking(GatewayTracking $tracking): static { - if (!$this->gatewayTrackings->contains($gatewayTracking)) { - $this->gatewayTrackings->add($gatewayTracking); - $gatewayTracking->setCheckout($this); - } + $this->gatewayTrackings = [...$this->gatewayTrackings, $tracking]; return $this; } - public function removeGatewayTracking(GatewayTracking $gatewayTracking): static + public function removeGatewayTracking(GatewayTracking $tracking): static { - if ($this->gatewayTrackings->removeElement($gatewayTracking)) { - // set the owning side to null (unless already changed) - if ($gatewayTracking->getCheckout() === $this) { - $gatewayTracking->setCheckout(null); + $this->gatewayTrackings = \array_filter( + $this->gatewayTrackings, + function (GatewayTracking $existingTracking) use ($tracking) { + return $existingTracking->title !== $tracking->title + && $existingTracking->value !== $tracking->value; } - } + ); return $this; } diff --git a/src/Entity/GatewayLink.php b/src/Entity/GatewayLink.php index 86db39b..6576424 100644 --- a/src/Entity/GatewayLink.php +++ b/src/Entity/GatewayLink.php @@ -2,114 +2,29 @@ namespace App\Entity; -use ApiPlatform\Metadata as API; -use App\Repository\GatewayLinkRepository; -use Doctrine\DBAL\Types\Types; -use Doctrine\ORM\Mapping as ORM; - -#[ORM\Entity(repositoryClass: GatewayLinkRepository::class)] class GatewayLink { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - private ?int $id = null; - /** * The complete target URL. */ - #[ORM\Column(type: Types::TEXT)] - private ?string $href = null; + public string $href; /** * The link relation type, which serves as an ID for a link that unambiguously describes the semantics of the link. * * @see https://www.iana.org/assignments/link-relations/link-relations.xhtml */ - #[ORM\Column(length: 255)] - private ?string $rel = null; + public string $rel; /** * The HTTP method required to make the related call. */ - #[ORM\Column(length: 255)] - private ?string $method = null; + public string $method; /** * The type of the link indicates who is the intended user of a link.\ * `debug` links are for developers and platform maintainers to get useful information about the checkout.\ * `payment` links are for end-users who must visit this link to complete the checkout. */ - #[ORM\Column(enumType: GatewayLinkType::class)] - private ?GatewayLinkType $type = null; - - #[API\ApiProperty(readable: false)] - #[ORM\ManyToOne(inversedBy: 'links')] - #[ORM\JoinColumn(nullable: false)] - private ?GatewayCheckout $checkout = null; - - public function getId(): ?int - { - return $this->id; - } - - public function getHref(): ?string - { - return $this->href; - } - - public function setHref(string $href): static - { - $this->href = $href; - - return $this; - } - - public function getRel(): ?string - { - return $this->rel; - } - - public function setRel(string $rel): static - { - $this->rel = $rel; - - return $this; - } - - public function getMethod(): ?string - { - return $this->method; - } - - public function setMethod(string $method): static - { - $this->method = $method; - - return $this; - } - - public function getType(): ?GatewayLinkType - { - return $this->type; - } - - public function setType(GatewayLinkType $type): static - { - $this->type = $type; - - return $this; - } - - public function getCheckout(): ?GatewayCheckout - { - return $this->checkout; - } - - public function setCheckout(?GatewayCheckout $checkout): static - { - $this->checkout = $checkout; - - return $this; - } + public GatewayLinkType $type; } diff --git a/src/Entity/GatewayTracking.php b/src/Entity/GatewayTracking.php index 0799870..b702411 100644 --- a/src/Entity/GatewayTracking.php +++ b/src/Entity/GatewayTracking.php @@ -2,71 +2,19 @@ namespace App\Entity; -use App\Repository\GatewayTrackingRepository; -use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; -#[ORM\Entity(repositoryClass: GatewayTrackingRepository::class)] class GatewayTracking { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - private ?int $id = null; + /** + * A descriptive title for the tracking number. + */ + #[Assert\NotBlank()] + public string $title; /** * The tracking number given by the Gateway. */ - #[ORM\Column(length: 255)] - private ?string $value = null; - - /** - * A descriptive title for the tracking number. - */ - #[ORM\Column(length: 255)] - private ?string $title = null; - - #[ORM\ManyToOne(inversedBy: 'gatewayIds')] - #[ORM\JoinColumn(nullable: false)] - private ?GatewayCheckout $checkout = null; - - public function getId(): ?int - { - return $this->id; - } - - public function getValue(): ?string - { - return $this->value; - } - - public function setValue(string $value): static - { - $this->value = $value; - - return $this; - } - - public function getTitle(): ?string - { - return $this->title; - } - - public function setTitle(string $title): static - { - $this->title = $title; - - return $this; - } - - public function getCheckout(): ?GatewayCheckout - { - return $this->checkout; - } - - public function setCheckout(?GatewayCheckout $checkout): static - { - $this->checkout = $checkout; - - return $this; - } + #[Assert\NotBlank()] + public string $value; } diff --git a/src/Repository/GatewayLinkRepository.php b/src/Repository/GatewayLinkRepository.php deleted file mode 100644 index 3207933..0000000 --- a/src/Repository/GatewayLinkRepository.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class GatewayLinkRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, GatewayLink::class); - } - - // /** - // * @return GatewayLink[] Returns an array of GatewayLink objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('g.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?GatewayLink - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } -} diff --git a/src/Repository/GatewayTrackingRepository.php b/src/Repository/GatewayTrackingRepository.php deleted file mode 100644 index f687a3f..0000000 --- a/src/Repository/GatewayTrackingRepository.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class GatewayTrackingRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, GatewayTracking::class); - } - - // /** - // * @return GatewayTracking[] Returns an array of GatewayTracking objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('g.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?GatewayTracking - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } -} From 35e221256f9d0ec773c1cd3c69ee5d2bcf5b86d0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 14:35:46 +0200 Subject: [PATCH 077/191] Remove unnecessary call to accounting setter on transactions --- src/Entity/AccountingTransaction.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Entity/AccountingTransaction.php b/src/Entity/AccountingTransaction.php index 4688f43..d3c3504 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/AccountingTransaction.php @@ -75,8 +75,6 @@ class AccountingTransaction public function setOrigin(?Accounting $origin): static { - $origin->addTransactionsIssued($this); - $this->origin = $origin; return $this; @@ -89,8 +87,6 @@ class AccountingTransaction public function setTarget(?Accounting $target): static { - $target->addTransactionsReceived($this); - $this->target = $target; return $this; From 1e5f6f3d6013df29a40f2120d347f7838d117ff7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 14:53:17 +0200 Subject: [PATCH 078/191] PHP CS Fixer fixes --- src/Entity/GatewayCheckout.php | 2 +- src/Library/Benzina/Pump/AbstractPump.php | 2 +- src/Library/Benzina/Pump/PumpInterface.php | 4 +- src/Service/GatewayCheckoutService.php | 59 +++++++++++++++++----- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php index f639d54..d483ce7 100644 --- a/src/Entity/GatewayCheckout.php +++ b/src/Entity/GatewayCheckout.php @@ -85,7 +85,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[] */ #[API\ApiProperty(writable: false)] diff --git a/src/Library/Benzina/Pump/AbstractPump.php b/src/Library/Benzina/Pump/AbstractPump.php index b89810a..0a2ee97 100644 --- a/src/Library/Benzina/Pump/AbstractPump.php +++ b/src/Library/Benzina/Pump/AbstractPump.php @@ -17,7 +17,7 @@ abstract class AbstractPump implements PumpInterface return [ $key => \array_key_exists($key, $this->config) ? $this->config[$key] - : $default + : $default, ]; } diff --git a/src/Library/Benzina/Pump/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index 043402e..8f84aa8 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -14,8 +14,8 @@ interface PumpInterface /** * Read the configuration values for this pump. * - * @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 + * @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 */ diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 10c460f..0ab5b3f 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -7,7 +7,6 @@ use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayCheckout; use App\Entity\GatewayCheckoutStatus; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Routing\RouterInterface; class GatewayCheckoutService @@ -17,10 +16,14 @@ class GatewayCheckoutService public function __construct( private RouterInterface $router, - private EntityManagerInterface $entityManager, ) { } + /** + * Generates a suitable value for redirection params to external gateways. + * + * @return string Absolute URL to the gateways redirection handler + */ public function generateRedirectUrl( GatewayCheckout $checkout, string $type = self::RESPONSE_TYPE_SUCCESS, @@ -38,17 +41,55 @@ class GatewayCheckoutService ); } - public function getGatewayReference(GatewayCheckout $checkout, GatewayCharge $charge): string + /** + * Generates a full tracking code for a given charge in a checkout. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateTracking(GatewayCheckout $checkout, GatewayCharge $charge): string { - return sprintf( - 'AO%d-CO%d-CH%d-AT%d', + return \sprintf( + '%s-%s', + $this->generateCheckoutTracking($checkout), + $this->generateChargeTracking($charge) + ); + } + + /** + * Generates a partial tracking code for any given checkout. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateCheckoutTracking(GatewayCheckout $checkout): string + { + return \sprintf( + 'AO%d-CO%d', $checkout->getOrigin()->getId(), - $checkout->getId(), + $checkout->getId() + ); + } + + /** + * Generates a partial tracking code for any given charge. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateChargeTracking(GatewayCharge $charge): string + { + return \sprintf( + 'CH%d-AT%d', $charge->getId(), $charge->getTarget()->getId() ); } + /** + * Updates a successfully charges GatewayCheckout and generates the transactions for each charge. + * + * @param GatewayCheckout $checkout The GatewayCheckout to be updated + * + * @return GatewayCheckout The updated GatewayCheckout with updated charges and generated transactions + */ public function chargeCheckout(GatewayCheckout $checkout): GatewayCheckout { $checkout->setStatus(GatewayCheckoutStatus::Charged); @@ -59,15 +100,9 @@ class GatewayCheckoutService $transaction->setOrigin($checkout->getOrigin()); $transaction->setTarget($charge->getTarget()); - $this->entityManager->persist($transaction); - $charge->setTransaction($transaction); - - $this->entityManager->persist($charge); } - $this->entityManager->flush(); - return $checkout; } } From b00aa525ade0c36cc83cffc85bf8a172a68d96fe Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 14:53:34 +0200 Subject: [PATCH 079/191] Fix checkout usage by checkouts pump --- src/Library/Benzina/Pump/CheckoutsPump.php | 37 ++++++++-------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index d52fc9d..3d50f4d 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -3,7 +3,6 @@ namespace App\Library\Benzina\Pump; use App\Entity\Accounting; -use App\Entity\AccountingTransaction; use App\Entity\GatewayCharge; use App\Entity\GatewayChargeType; use App\Entity\GatewayCheckout; @@ -22,6 +21,7 @@ use App\Library\Economy\Payment\WalletGateway; use App\Repository\ProjectRepository; use App\Repository\TipjarRepository; use App\Repository\UserRepository; +use App\Service\GatewayCheckoutService; use Doctrine\ORM\EntityManagerInterface; class CheckoutsPump extends AbstractPump implements PumpInterface @@ -75,6 +75,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, private EntityManagerInterface $entityManager, + private GatewayCheckoutService $checkoutService, ) { } @@ -122,11 +123,6 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->setMigrated(true); $checkout->setMigratedId($record['id']); - $checkout->setMetadata([ - 'payment' => $record['payment'], - 'transaction' => $record['transaction'], - 'preapproval' => $record['preapproval'], - ]); $checkout->setDateCreated(new \DateTime($record['invested'])); $checkout->setDateUpdated(new \DateTime()); @@ -145,6 +141,8 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $charge->setTarget($project->getAccounting()); } + $checkout->addCharge($charge); + if ($record['donate_amount'] > 0) { $tip = new GatewayCharge(); $tip->setType(GatewayChargeType::Single); @@ -154,17 +152,8 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout->addCharge($tip); } - $checkout->addCharge($charge); - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { - foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); - - $charge->setTransaction($transaction); - } + $checkout = $this->checkoutService->chargeCheckout($checkout); } $this->entityManager->persist($checkout); @@ -284,31 +273,31 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private function getCheckoutTrackings(array $record): array { $v3Tracking = new GatewayTracking(); - $v3Tracking->setValue($record['id']); - $v3Tracking->setTitle(self::TRACKING_TITLE_V3); + $v3Tracking->title = self::TRACKING_TITLE_V3; + $v3Tracking->value = $record['id']; $trackings = [$v3Tracking]; if (!empty($record['payment'])) { $payment = new GatewayTracking(); - $payment->setValue($record['payment']); - $payment->setTitle(self::TRACKING_TITLE_PAYMENT); + $payment->title = self::TRACKING_TITLE_PAYMENT; + $payment->value = $record['payment']; $trackings[] = $payment; } if (!empty($record['transaction'])) { $transaction = new GatewayTracking(); - $transaction->setValue($record['transaction']); - $transaction->setTitle(self::TRACKING_TITLE_TRANSACTION); + $transaction->title = self::TRACKING_TITLE_TRANSACTION; + $transaction->value = $record['transaction']; $trackings[] = $transaction; } if (!empty($record['preapproval'])) { $preapproval = new GatewayTracking(); - $preapproval->setValue($record['preapproval']); - $preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL); + $preapproval->title = self::TRACKING_TITLE_PREAPPROVAL; + $preapproval->value = $record['preapproval']; $trackings[] = $preapproval; } From 1a0cb373841514cb81fc4246b1b6bd00430fb9d3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 19:35:31 +0200 Subject: [PATCH 080/191] Remove estimated time from pump command --- src/Command/BenzinaPumpCommand.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 40b5016..4b6ac86 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -108,9 +108,6 @@ EOF return $pump::class; }, $pumps)); - $etaSection = $output->section(); - $etaSection->writeln('ETA'); - $streamedBatchesSection = $output->section(); $streamedBatchesSection->writeln("\tStreamed batches:"); $streamedBatches = new ProgressBar($streamedBatchesSection); @@ -130,7 +127,6 @@ EOF \memory_reset_peak_usage(); $batch = $stream->read(); - $batchStartTime = \microtime(true); $streamBatches = $streamBatches - 1; $streamedBatches->advance(); @@ -147,12 +143,6 @@ EOF $streamed = $stream->tell(); $streamedRecords->setProgress($streamed); - - $batchEndTime = \microtime(true); - $batchRunTime = $batchEndTime - $batchStartTime; - - $eta = round($streamBatches * $batchRunTime); - $etaSection->overwrite(sprintf('ETA %02d:%02d:%02d', $eta / 3600, floor($eta / 60) % 60, $eta % 60)); } $endSection = new SymfonyStyle($input, $output->section()); From 9187d7c5270efabe562fb9478b5a91f219e667ce Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 21:08:39 +0200 Subject: [PATCH 081/191] Update docblock --- src/Service/GatewayCheckoutService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/GatewayCheckoutService.php b/src/Service/GatewayCheckoutService.php index 0ab5b3f..54d049b 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/GatewayCheckoutService.php @@ -84,7 +84,7 @@ class GatewayCheckoutService } /** - * Updates a successfully charges GatewayCheckout and generates the transactions for each charge. + * Updates a successful GatewayCheckout and generates the transactions for each charge. * * @param GatewayCheckout $checkout The GatewayCheckout to be updated * From 5e425442cbdf1e79b44c73b4125563d17a91775f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 10 Oct 2024 21:08:53 +0200 Subject: [PATCH 082/191] Simplify accounting --- src/Entity/Accounting.php | 159 +++++++++++++-------------- src/Entity/AccountingTransaction.php | 4 - tests/Entity/AccountingTest.php | 118 ++++---------------- 3 files changed, 96 insertions(+), 185 deletions(-) diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index 8a7b2c4..4517e11 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -31,6 +31,20 @@ class Accounting #[ORM\Column(length: 3)] private ?string $currency = null; + /** + * @var Collection + */ + #[API\ApiProperty(writable: false)] + #[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)] + private Collection $transactionsIssued; + + /** + * @var Collection + */ + #[API\ApiProperty(writable: false)] + #[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)] + private Collection $transactionsReceived; + #[API\ApiProperty(writable: false, readable: false)] #[ORM\Column(length: 255)] private ?string $ownerClass = null; @@ -47,14 +61,6 @@ class Accounting #[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])] private ?Tipjar $tipjar = null; - #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)] - private Collection $transactionsIssued; - - #[API\ApiProperty(writable: false)] - #[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)] - private Collection $transactionsReceived; - public function __construct() { /* @@ -83,6 +89,66 @@ class Accounting return $this; } + /** + * @return Collection + */ + public function getTransactionsIssued(): Collection + { + return $this->transactionsIssued; + } + + public function addTransactionsIssued(AccountingTransaction $transactionsIssued): static + { + if (!$this->transactionsIssued->contains($transactionsIssued)) { + $this->transactionsIssued->add($transactionsIssued); + $transactionsIssued->setOrigin($this); + } + + return $this; + } + + public function removeTransactionsIssued(AccountingTransaction $transactionsIssued): static + { + if ($this->transactionsIssued->removeElement($transactionsIssued)) { + // set the owning side to null (unless already changed) + if ($transactionsIssued->getOrigin() === $this) { + $transactionsIssued->setOrigin(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getTransactionsReceived(): Collection + { + return $this->transactionsReceived; + } + + public function addTransactionsReceived(AccountingTransaction $transactionsReceived): static + { + if (!$this->transactionsReceived->contains($transactionsReceived)) { + $this->transactionsReceived->add($transactionsReceived); + $transactionsReceived->setTarget($this); + } + + return $this; + } + + public function removeTransactionsReceived(AccountingTransaction $transactionsReceived): static + { + if ($this->transactionsReceived->removeElement($transactionsReceived)) { + // set the owning side to null (unless already changed) + if ($transactionsReceived->getTarget() === $this) { + $transactionsReceived->setTarget(null); + } + } + + return $this; + } + public function getOwnerClass(): ?string { return $this->ownerClass; @@ -167,81 +233,4 @@ class Accounting return $this; } - - /** - * @return Collection - */ - public function getTransactions(): Collection - { - $transactions = [ - ...$this->getTransactionsIssued()->toArray(), - ...$this->getTransactionsReceived()->toArray(), - ]; - - \usort($transactions, function ($a, $b) { - return $a->getId() - $b->getId(); - }); - - return new ArrayCollection($transactions); - } - - /** - * @return Collection - */ - public function getTransactionsIssued(): Collection - { - return $this->transactionsIssued; - } - - public function addTransactionsIssued(AccountingTransaction $transaction): static - { - if (!$this->transactionsIssued->contains($transaction)) { - $this->transactionsIssued->add($transaction); - $transaction->setOrigin($this); - } - - return $this; - } - - public function removeTransactionsIssued(AccountingTransaction $transaction): static - { - if ($this->transactionsIssued->removeElement($transaction)) { - // set the owning side to null (unless already changed) - if ($transaction->getOrigin() === $this) { - $transaction->setOrigin(null); - } - } - - return $this; - } - - /** - * @return Collection - */ - public function getTransactionsReceived(): Collection - { - return $this->transactionsReceived; - } - - public function addTransactionsReceived(AccountingTransaction $transaction): static - { - if (!$this->transactionsReceived->contains($transaction)) { - $this->transactionsReceived->add($transaction); - $transaction->setTarget($this); - } - - return $this; - } - - public function removeTransactionsReceived(AccountingTransaction $transaction): static - { - if ($this->transactionsReceived->removeElement($transaction)) { - // set the owning side to null (unless already changed) - if ($transaction->getTarget() === $this) { - $transaction->setTarget(null); - } - } - - return $this; - } } diff --git a/src/Entity/AccountingTransaction.php b/src/Entity/AccountingTransaction.php index d3c3504..6cc43a6 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/AccountingTransaction.php @@ -36,8 +36,6 @@ class AccountingTransaction /** * The Accounting issuing the money of this Transaction. */ - #[Assert\NotBlank()] - #[API\ApiProperty(writable: false)] #[ORM\ManyToOne(inversedBy: 'transactionsIssued', cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] private ?Accounting $origin = null; @@ -45,8 +43,6 @@ class AccountingTransaction /** * The Accounting receiving the money of this Transaction. */ - #[Assert\NotBlank()] - #[API\ApiProperty(writable: false)] #[ORM\ManyToOne(inversedBy: 'transactionsReceived', cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] private ?Accounting $target = null; diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php index d0e6191..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; @@ -23,114 +21,42 @@ class AccountingTest extends KernelTestCase $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); } - public function testAccountingGetsUpdatedByPersistingTransactions() + public function testAccountingGetsUpdatedByCascade() { $tipjarA = new Tipjar(); $tipjarA->setName('TEST_TIPJAR_A'); - $accA = new Accounting(); - $accA->setTipjar($tipjarA); + $accountingA = new Accounting(); + $accountingA->setTipjar($tipjarA); + + $this->entityManager->persist($tipjarA); + $this->entityManager->flush(); + + /** @var Accounting */ + $accountingA = $this->entityManager->getRepository(Tipjar::class) + ->findOneBy(['name' => 'TEST_TIPJAR_A']) + ->getAccounting(); + + $this->assertNotNull($accountingA->getId()); + $this->assertEquals(Tipjar::class, $accountingA->getOwnerClass()); + $this->assertEquals('TEST_TIPJAR_A', $accountingA->getTipjar()->getName()); $tipjarB = new Tipjar(); $tipjarB->setName('TEST_TIPJAR_B'); - $accB = new Accounting(); - $accB->setTipjar($tipjarB); + $accountingB = new Accounting(); + $accountingB->setTipjar($tipjarB); - $trx = new AccountingTransaction(); - $trx->setMoney(new Money(120, 'EUR')); - $trx->setOrigin($accA); - $trx->setTarget($accB); - - $this->entityManager->persist($trx); + $this->entityManager->persist($tipjarB); $this->entityManager->flush(); /** @var Accounting */ - $accA = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_A']) - ->getAccounting(); - - /** @var Accounting */ - $accB = $this->entityManager->getRepository(Tipjar::class) + $accountingB = $this->entityManager->getRepository(Tipjar::class) ->findOneBy(['name' => 'TEST_TIPJAR_B']) ->getAccounting(); - $this->assertNotNull($accA->getId()); - $this->assertNotNull($accB->getId()); - - $this->assertCount(1, $accA->getTransactionsIssued()); - $this->assertCount(0, $accA->getTransactionsReceived()); - - $this->assertCount(0, $accB->getTransactionsIssued()); - $this->assertCount(1, $accB->getTransactionsReceived()); - - $this->assertEquals(120, $accA->getTransactionsIssued()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accA->getTransactionsIssued()[0]->getMoney()->currency); - - $this->assertEquals(120, $accB->getTransactionsReceived()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accB->getTransactionsReceived()[0]->getMoney()->currency); - } - - public function testAccountingSortsMixedTransactions() - { - $tipjarA = new Tipjar(); - $tipjarA->setName('TEST_TIPJAR_A'); - - $accA = new Accounting(); - $accA->setTipjar($tipjarA); - - $tipjarB = new Tipjar(); - $tipjarB->setName('TEST_TIPJAR_B'); - - $accB = new Accounting(); - $accB->setTipjar($tipjarB); - - $trx1 = new AccountingTransaction(); - $trx1->setMoney(new Money(1, 'EUR')); - $trx1->setOrigin($accA); - $trx1->setTarget($accB); - - $trx2 = new AccountingTransaction(); - $trx2->setMoney(new Money(2, 'USD')); - $trx2->setOrigin($accB); - $trx2->setTarget($accA); - - $this->entityManager->persist($trx1); - $this->entityManager->persist($trx2); - $this->entityManager->flush(); - - /** @var Accounting */ - $accA = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_A']) - ->getAccounting(); - - /** @var Accounting */ - $accB = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_B']) - ->getAccounting(); - - $this->assertNotNull($accA->getId()); - $this->assertNotNull($accB->getId()); - - $this->assertCount(2, $accA->getTransactions()); - $this->assertCount(2, $accB->getTransactions()); - - $this->assertEquals(1, $accA->getTransactions()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accA->getTransactions()[0]->getMoney()->currency); - - $this->assertEquals(1, $accB->getTransactions()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accB->getTransactions()[0]->getMoney()->currency); - - $this->assertEquals(2, $accA->getTransactions()[1]->getMoney()->amount); - $this->assertEquals('USD', $accA->getTransactions()[1]->getMoney()->currency); - - $this->assertEquals(2, $accB->getTransactions()[1]->getMoney()->amount); - $this->assertEquals('USD', $accB->getTransactions()[1]->getMoney()->currency); - - $this->assertEquals($accA, $accA->getTransactions()[0]->getOrigin()); - $this->assertEquals($accA, $accA->getTransactions()[1]->getTarget()); - - $this->assertEquals($accB, $accB->getTransactions()[1]->getOrigin()); - $this->assertEquals($accB, $accB->getTransactions()[0]->getTarget()); + $this->assertNotNull($accountingB->getId()); + $this->assertEquals(Tipjar::class, $accountingB->getOwnerClass()); + $this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName()); } } From 6e95ac42c97f640f2a45b53d97abe4824435d8df Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Fri, 11 Oct 2024 11:27:04 +0200 Subject: [PATCH 083/191] 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 084/191] 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 085/191] 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 086/191] 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 087/191] 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 088/191] 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 089/191] 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 090/191] 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 091/191] 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 092/191] 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 093/191] 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 094/191] 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 095/191] 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 096/191] 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 097/191] 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 098/191] 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 099/191] 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 100/191] 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 101/191] 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 102/191] 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 103/191] 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 104/191] 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 105/191] 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 106/191] 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 107/191] 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 108/191] 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 109/191] 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 110/191] 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 111/191] 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 112/191] 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 113/191] 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 114/191] 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 115/191] 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 116/191] 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 117/191] 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 118/191] 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 119/191] 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 120/191] 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 121/191] 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 122/191] 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 123/191] 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 124/191] 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 125/191] 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 126/191] 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 127/191] 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 128/191] 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 129/191] 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 130/191] 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 131/191] 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 132/191] 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 133/191] 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 134/191] 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 135/191] 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 136/191] 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 137/191] 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 138/191] 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 139/191] 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 140/191] 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 141/191] 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 142/191] 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 143/191] 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 144/191] 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 145/191] 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 146/191] 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 147/191] 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 148/191] 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 149/191] 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 150/191] 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 151/191] 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 152/191] 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 153/191] 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 154/191] 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 155/191] 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 156/191] 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 157/191] 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 158/191] 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 159/191] 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 160/191] 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 161/191] 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 162/191] 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 163/191] 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 164/191] 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 165/191] 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 166/191] 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 167/191] 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 168/191] 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() From 21a02dd5ad9fcb76a176c4b71ca0df02250f6121 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 12:34:34 +0100 Subject: [PATCH 169/191] Fix namespace usage in checkout service --- src/Service/Gateway/CheckoutService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Gateway/CheckoutService.php b/src/Service/Gateway/CheckoutService.php index 2ad5ac7..94a10b7 100644 --- a/src/Service/Gateway/CheckoutService.php +++ b/src/Service/Gateway/CheckoutService.php @@ -6,7 +6,7 @@ use App\Controller\GatewaysController; use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; -use App\Entity\Gateway\CheckoutStatus; +use App\Gateway\CheckoutStatus; use Symfony\Component\Routing\RouterInterface; class CheckoutService From 8edb93e2857ff8fc859f1d0e3c83020d3a434b9a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 12:41:29 +0100 Subject: [PATCH 170/191] Charge wallet processed checkouts --- src/Gateway/Wallet/WalletGateway.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index d3abded..2ca2f2c 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -9,6 +9,8 @@ use App\Entity\WalletStatement; use App\Gateway\ChargeType; use App\Gateway\GatewayInterface; use App\Library\Economy\MoneyService; +use App\Service\Gateway\CheckoutService; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -30,15 +32,16 @@ class WalletGateway implements GatewayInterface public function __construct( private WalletService $wallet, private MoneyService $money, + private CheckoutService $checkoutService, + private EntityManagerInterface $entityManager, ) {} public function process(Checkout $checkout): Checkout { $origin = $checkout->getOrigin(); $available = $this->wallet->getBalance($origin); - $chargeTotal = $this->getChargeTotal($checkout); - if ($this->money->isLess($available, $chargeTotal)) { + if ($this->money->isLess($available, $this->getChargeTotal($checkout))) { throw new \Exception("Can't spend more than what you have!"); } @@ -56,6 +59,11 @@ class WalletGateway implements GatewayInterface $outgoing = $this->wallet->spend($transaction); } + $checkout = $this->checkoutService->chargeCheckout($checkout); + + $this->entityManager->persist($checkout); + $this->entityManager->flush(); + return $checkout; } From 6139ba697f663fb3bdb26e82ac7465ff73e09a59 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 13:35:31 +0100 Subject: [PATCH 171/191] Small bug fixes --- src/Entity/Accounting/Accounting.php | 2 +- src/Entity/WalletFinancement.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php index 9bb5665..1695bf7 100644 --- a/src/Entity/Accounting/Accounting.php +++ b/src/Entity/Accounting/Accounting.php @@ -69,7 +69,7 @@ class Accounting return $this; } - public function getOwner(): AccountingOwnerInterface + public function getOwner(): ?AccountingOwnerInterface { $owner = $this->owner; diff --git a/src/Entity/WalletFinancement.php b/src/Entity/WalletFinancement.php index ea13819..34f7ddc 100644 --- a/src/Entity/WalletFinancement.php +++ b/src/Entity/WalletFinancement.php @@ -25,7 +25,7 @@ class WalletFinancement /** * An incoming Statement that originally saved the money in this Financement. */ - #[ORM\ManyToOne(inversedBy: 'financesto')] + #[ORM\ManyToOne(inversedBy: 'financesTo')] #[ORM\JoinColumn(nullable: false)] private ?WalletStatement $origin = null; From 74361ec330f542de0a6aae54d12ce1f49bdfe703 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 13:36:21 +0100 Subject: [PATCH 172/191] Update entity checkout gateway to specify gateway name property --- src/Entity/Gateway/Checkout.php | 10 +++++----- src/Gateway/GatewayLocator.php | 2 +- src/Library/Benzina/Pump/CheckoutsPump.php | 2 +- src/Mapping/Gateway/CheckoutMapper.php | 4 ++-- src/Service/Gateway/CheckoutService.php | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index 6add15e..8394c43 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -68,7 +68,7 @@ class Checkout */ #[Assert\NotBlank()] #[ORM\Column(length: 255)] - private ?string $gateway = null; + private ?string $gatewayName = null; /** * A list of URLs provided by the Gateway for this checkout.\ @@ -168,14 +168,14 @@ class Checkout return $this; } - public function getGateway(): ?string + public function getGatewayName(): ?string { - return $this->gateway; + return $this->gatewayName; } - public function setGateway(string $gateway): static + public function setGatewayName(string $gatewayName): static { - $this->gateway = $gateway; + $this->gatewayName = $gatewayName; return $this; } diff --git a/src/Gateway/GatewayLocator.php b/src/Gateway/GatewayLocator.php index 99eca0d..952bbd9 100644 --- a/src/Gateway/GatewayLocator.php +++ b/src/Gateway/GatewayLocator.php @@ -68,7 +68,7 @@ class GatewayLocator */ public function getGatewayOf(Checkout $checkout): GatewayInterface { - $gateway = $checkout->getGateway(); + $gateway = $checkout->getGatewayName(); if (!$gateway) { throw new \Exception('The given GatewayCheckout does not specify a Gateway'); } diff --git a/src/Library/Benzina/Pump/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index e7d461e..cf54a5b 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -117,7 +117,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $checkout = new Checkout(); $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); - $checkout->setGateway($this->getCheckoutGateway($record)); + $checkout->setGatewayName($this->getCheckoutGateway($record)); foreach ($this->getCheckoutTrackings($record) as $tracking) { $checkout->addTracking($tracking); diff --git a/src/Mapping/Gateway/CheckoutMapper.php b/src/Mapping/Gateway/CheckoutMapper.php index c7f24b8..53dfc20 100644 --- a/src/Mapping/Gateway/CheckoutMapper.php +++ b/src/Mapping/Gateway/CheckoutMapper.php @@ -24,7 +24,7 @@ class CheckoutMapper $resource = new Resource\Checkout(); $resource->id = $entity->getId(); - $gateway = $this->gatewayLocator->getGateway($entity->getGateway()); + $gateway = $this->gatewayLocator->getGatewayOf($entity); $resource->gateway = $this->gatewayMapper->toResource($gateway); $resource->origin = $this->accountingMapper->toResource($entity->getOrigin()); @@ -51,7 +51,7 @@ class CheckoutMapper $entity = $this->checkoutRepository->find($resource->id); } - $entity->setGateway($resource->gateway->name); + $entity->setGatewayName($resource->gateway->name); $entity->setOrigin($this->accountingMapper->toEntity($resource->origin)); $charges = []; diff --git a/src/Service/Gateway/CheckoutService.php b/src/Service/Gateway/CheckoutService.php index 94a10b7..0b0cd80 100644 --- a/src/Service/Gateway/CheckoutService.php +++ b/src/Service/Gateway/CheckoutService.php @@ -32,7 +32,7 @@ class CheckoutService GatewaysController::REDIRECT, [ 'type' => $type, - 'gateway' => $checkout->getGateway(), + 'gateway' => $checkout->getGatewayName(), 'checkoutId' => $checkout->getId(), ...$parameters, ], From c4ae91085230ac530c785bba62c7682c15c3e098 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 13:40:20 +0100 Subject: [PATCH 173/191] Add provider for checkout --- src/ApiResource/Gateway/Checkout.php | 4 +- src/State/Gateway/CheckoutStateProvider.php | 44 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/State/Gateway/CheckoutStateProvider.php diff --git a/src/ApiResource/Gateway/Checkout.php b/src/ApiResource/Gateway/Checkout.php index 8fb7780..f50e219 100644 --- a/src/ApiResource/Gateway/Checkout.php +++ b/src/ApiResource/Gateway/Checkout.php @@ -10,6 +10,7 @@ use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\Tracking; use App\State\Gateway\CheckoutStateProcessor; +use App\State\Gateway\CheckoutStateProvider; use Symfony\Component\Validator\Constraints as Assert; /** @@ -18,7 +19,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\ApiResource( shortName: 'GatewayCheckout', stateOptions: new Options(entityClass: Entity\Checkout::class), - processor: CheckoutStateProcessor::class + provider: CheckoutStateProvider::class, + processor: CheckoutStateProcessor::class, )] #[API\GetCollection()] #[API\Post()] diff --git a/src/State/Gateway/CheckoutStateProvider.php b/src/State/Gateway/CheckoutStateProvider.php new file mode 100644 index 0000000..0d2d3b9 --- /dev/null +++ b/src/State/Gateway/CheckoutStateProvider.php @@ -0,0 +1,44 @@ +collectionProvider->provide($operation, $uriVariables, $context); + + $resources = []; + foreach ($entities as $entity) { + $resources[] = $this->checkoutMapper->toResource($entity); + } + + return $resources; + } + + $entity = $this->itemProvider->provide($operation, $uriVariables, $context); + + if ($entity === null) { + return null; + } + + return $this->checkoutMapper->toResource($entity); + } +} From e318a2da17013c575110f35504e08f457105d094 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 18 Nov 2024 13:58:40 +0100 Subject: [PATCH 174/191] Simplify gateway locator method names --- src/Controller/GatewaysController.php | 4 +-- src/Gateway/GatewayLocator.php | 28 ++++--------------- src/Mapping/Gateway/CheckoutMapper.php | 2 +- src/State/Gateway/CheckoutStateProcessor.php | 2 +- src/State/Gateway/GatewayStateProvider.php | 4 +-- .../SupportedChargeTypesValidator.php | 2 +- 6 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/Controller/GatewaysController.php b/src/Controller/GatewaysController.php index 598a154..9ef8c5e 100644 --- a/src/Controller/GatewaysController.php +++ b/src/Controller/GatewaysController.php @@ -24,7 +24,7 @@ class GatewaysController extends AbstractController #[Route('/gateway_redirects', name: self::REDIRECT)] public function handleRedirect(Request $request): Response { - $gateway = $this->gatewayLocator->getGateway($request->query->get('gateway')); + $gateway = $this->gatewayLocator->get($request->query->get('gateway')); return $gateway->handleRedirect($request); } @@ -32,7 +32,7 @@ class GatewaysController extends AbstractController #[Route('/gateway_webhooks', name: self::WEBHOOKS)] public function handleWebhook(Request $request): Response { - $gateway = $this->gatewayLocator->getGateway($request->query->get('gateway')); + $gateway = $this->gatewayLocator->get($request->query->get('gateway')); return $gateway->handleWebhook($request); } diff --git a/src/Gateway/GatewayLocator.php b/src/Gateway/GatewayLocator.php index 952bbd9..f36bcc6 100644 --- a/src/Gateway/GatewayLocator.php +++ b/src/Gateway/GatewayLocator.php @@ -25,26 +25,10 @@ class GatewayLocator } } - /** - * @return array List of the fully-qualified class names of the available interfaces - */ - public function getClasses(): array - { - return \array_keys($this->gatewaysByClass); - } - - /** - * @return array List of names of the available interfaces - */ - public function getNames(): array - { - return \array_keys($this->gatewaysByName); - } - /** * @return GatewayInterface[] */ - public function getGateways(): array + public function getAll(): array { return $this->gatewaysByName; } @@ -52,12 +36,12 @@ class GatewayLocator /** * @param string $name Name of the Gateway interface implementation * - * @throws \Exception When the $name does not match to that of an implemented Gateway + * @throws \Exception When the `$name` does not match to that of an implemented Gateway */ - public function getGateway(string $name): GatewayInterface + public function get(string $name): GatewayInterface { if (!\array_key_exists($name, $this->gatewaysByName)) { - throw new \Exception("No such Gateway with the name $name"); + throw new \Exception("Could not match '$name' to the name of any available Gateway implementation"); } return $this->gatewaysByName[$name]; @@ -66,14 +50,14 @@ class GatewayLocator /** * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway */ - public function getGatewayOf(Checkout $checkout): GatewayInterface + public function getByCheckout(Checkout $checkout): GatewayInterface { $gateway = $checkout->getGatewayName(); if (!$gateway) { throw new \Exception('The given GatewayCheckout does not specify a Gateway'); } - return $this->getGateway($gateway); + return $this->get($gateway); } /** diff --git a/src/Mapping/Gateway/CheckoutMapper.php b/src/Mapping/Gateway/CheckoutMapper.php index 53dfc20..0bd81f6 100644 --- a/src/Mapping/Gateway/CheckoutMapper.php +++ b/src/Mapping/Gateway/CheckoutMapper.php @@ -24,7 +24,7 @@ class CheckoutMapper $resource = new Resource\Checkout(); $resource->id = $entity->getId(); - $gateway = $this->gatewayLocator->getGatewayOf($entity); + $gateway = $this->gatewayLocator->getByCheckout($entity); $resource->gateway = $this->gatewayMapper->toResource($gateway); $resource->origin = $this->accountingMapper->toResource($entity->getOrigin()); diff --git a/src/State/Gateway/CheckoutStateProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php index f8d7509..b11ae0c 100644 --- a/src/State/Gateway/CheckoutStateProcessor.php +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -29,7 +29,7 @@ class CheckoutStateProcessor implements ProcessorInterface $entity = $this->checkoutMapper->toEntity($data); $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); - $entity = $this->gatewayLocator->getGateway($data->gateway->name)->process($entity); + $entity = $this->gatewayLocator->get($data->gateway->name)->process($entity); $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); return $this->checkoutMapper->toResource($entity); diff --git a/src/State/Gateway/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php index 803a7e9..4c3e477 100644 --- a/src/State/Gateway/GatewayStateProvider.php +++ b/src/State/Gateway/GatewayStateProvider.php @@ -30,7 +30,7 @@ class GatewayStateProvider implements ProviderInterface private function getGateways(): array { $gateways = []; - foreach ($this->gateways->getGateways() as $gateway) { + foreach ($this->gateways->getAll() as $gateway) { $gateways[] = $this->gatewayMapper->toResource($gateway); } @@ -40,7 +40,7 @@ class GatewayStateProvider implements ProviderInterface private function getGateway(string $name): Gateway { try { - $gateway = $this->gateways->getGateway($name); + $gateway = $this->gateways->get($name); return $this->gatewayMapper->toResource($gateway); } catch (\Exception $e) { diff --git a/src/Validator/SupportedChargeTypesValidator.php b/src/Validator/SupportedChargeTypesValidator.php index ad3be30..d0398a7 100644 --- a/src/Validator/SupportedChargeTypesValidator.php +++ b/src/Validator/SupportedChargeTypesValidator.php @@ -27,7 +27,7 @@ class SupportedChargeTypesValidator extends ConstraintValidator $charges = $value; $checkout = $charges->toArray()[0]->getCheckout(); - $gateway = $this->gatewayLocator->getGatewayOf($checkout); + $gateway = $this->gatewayLocator->getByCheckout($checkout); foreach ($charges as $charge) { if (!\in_array( From 11cff7cfdc5a240d3a9beb34d60be39e85e87755 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 14:15:32 +0100 Subject: [PATCH 175/191] Update Wallet tests --- tests/Gateway/Wallet/WalletServiceTest.php | 72 ++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index d1cb253..72255bc 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -51,7 +51,54 @@ class WalletServiceTest extends KernelTestCase return $tipjar; } - public function testTransactionsAddFunds() + public function testSaveAddFunds() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $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); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $income = $this->walletService->save($incoming); + + $this->assertEquals(100, $income->getBalance()->amount); + $this->assertEquals(StatementDirection::Incoming->value, $income->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $income->getFinancesTo()); + $this->assertCount(0, $income->getFinancedBy()); + + $this->entityManager->persist($income); + $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(StatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + } + + public function testTransactionsListenerAddFunds() { $tipjar = $this->getTipjar()->getAccounting(); $user = $this->getUser()->getAccounting(); @@ -70,12 +117,6 @@ class WalletServiceTest extends KernelTestCase $incoming->setOrigin($tipjar); $incoming->setTarget($user); - /* - * $this->walletService->save($incoming); - * called automatically on Transaction persist. - * - * @see \App\EventListener\WalletTransactionsListener - */ $this->entityManager->persist($incoming); $this->entityManager->flush(); @@ -96,7 +137,7 @@ class WalletServiceTest extends KernelTestCase $this->assertCount(0, $statement->getFinancedBy()); } - public function testTransactionsGetFinanced() + public function testSpendFinancesExpenses() { $tipjar = $this->getTipjar()->getAccounting(); $user = $this->getUser()->getAccounting(); @@ -114,7 +155,10 @@ class WalletServiceTest extends KernelTestCase $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); - $this->walletService->spend($outgoing); + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); $balance = $this->walletService->getBalance($user); @@ -144,7 +188,10 @@ class WalletServiceTest extends KernelTestCase $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); - $this->walletService->spend($outgoing); + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); $balance = $this->walletService->getBalance($user); @@ -215,7 +262,10 @@ class WalletServiceTest extends KernelTestCase $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); - $this->walletService->spend($outgoing); + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); $incoming = new Transaction(); $incoming->setMoney(new Money(13, 'EUR')); From af4c7cb4a2f9258af9dc2c008dd63a6664ec188b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 14:16:34 +0100 Subject: [PATCH 176/191] Remove as much explicit persistence from wallet as possible --- src/Entity/WalletStatement.php | 2 +- .../WalletTransactionsListener.php | 17 ++++++++++------ src/Gateway/Wallet/WalletService.php | 20 +++++++++---------- src/Repository/WalletStatementRepository.php | 15 ++++++++++++++ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index 08f0ac9..e1e5a49 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -27,7 +27,7 @@ class WalletStatement #[ORM\Column] private ?int $id = null; - #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\OneToOne(cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] private ?Transaction $transaction = null; diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index f868e65..33e6939 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -6,7 +6,6 @@ use App\Entity\Accounting\Transaction; use App\Entity\User; use App\Gateway\Wallet\WalletService; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Events; @@ -18,7 +17,6 @@ use Doctrine\ORM\Events; final class WalletTransactionsListener { public function __construct( - private EntityManagerInterface $entityManager, private WalletService $wallet, ) {} @@ -29,10 +27,17 @@ final class WalletTransactionsListener Transaction $transaction, PostPersistEventArgs $event, ) { - $target = $transaction->getTarget(); - - if ($target->getOwner() instanceof User) { - $this->wallet->save($transaction); + if (!$transaction->getTarget()->getOwner() instanceof User) { + return; } + + $statement = $this->wallet->save($transaction); + + if ($statement->getId() !== null) { + return; + } + + $event->getObjectManager()->persist($statement); + $event->getObjectManager()->flush(); } } diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index b64f153..763ad12 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -16,7 +16,7 @@ class WalletService { public function __construct( private MoneyService $money, - private WalletStatementRepository $walletStatementRepository, + private WalletStatementRepository $statementRepository, private EntityManagerInterface $entityManager, ) {} @@ -25,7 +25,7 @@ class WalletService */ public function getStatements(Accounting $accounting): array { - return $this->walletStatementRepository->findByAccounting($accounting); + return $this->statementRepository->findByAccounting($accounting); } public function getBalance(Accounting $accounting): Money @@ -47,22 +47,25 @@ class WalletService } /** - * Puts the money of a Transaction into the target wallet. + * Obtain an `incoming` WalletStatement for the Transaction. * * @param Transaction $transaction The Transaction targetting a wallet * - * @return WalletStatement An incoming statement for the target wallet + * @return WalletStatement An incoming statement for the transaction */ public function save(Transaction $transaction): WalletStatement { + $statement = $this->statementRepository->findByTransaction($transaction); + + if ($statement) { + return $statement; + } + $statement = new WalletStatement(); $statement->setTransaction($transaction); $statement->setDirection(StatementDirection::Incoming); $statement->setBalance($transaction->getMoney()); - $this->entityManager->persist($statement); - $this->entityManager->flush(); - return $statement; } @@ -117,14 +120,11 @@ class WalletService $outgoing->addFinancedBy($financement); $this->entityManager->persist($incoming); - $this->entityManager->persist($outgoing); $spendGoal = $this->money->substract($balanceSpent, $spendGoal); $spentTotal = $this->money->add($balanceSpent, $spentTotal); } - $this->entityManager->flush(); - return $outgoing; } } diff --git a/src/Repository/WalletStatementRepository.php b/src/Repository/WalletStatementRepository.php index 8987513..755eee7 100644 --- a/src/Repository/WalletStatementRepository.php +++ b/src/Repository/WalletStatementRepository.php @@ -3,6 +3,7 @@ namespace App\Repository; use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; use App\Entity\WalletStatement; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query\Expr\Join; @@ -64,6 +65,20 @@ class WalletStatementRepository extends ServiceEntityRepository ; } + public function findByTransaction(Transaction $transaction): ?WalletStatement + { + if ($transaction->getId() === null) { + return null; + } + + return $this->createQueryBuilder('w') + ->andWhere('w.transaction = :val') + ->setParameter('val', $transaction) + ->getQuery() + ->getOneOrNullResult() + ; + } + // /** // * @return WalletStatement[] Returns an array of WalletStatement objects // */ From b4dcd9571c95dee730095303e2769bf8d2d8d311 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 14:52:15 +0100 Subject: [PATCH 177/191] Consistent save = income / spend = expenditure var naming for wallet usage --- src/EventListener/WalletTransactionsListener.php | 8 ++++---- src/Gateway/Wallet/WalletGateway.php | 10 +++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php index 33e6939..d8c845a 100644 --- a/src/EventListener/WalletTransactionsListener.php +++ b/src/EventListener/WalletTransactionsListener.php @@ -21,7 +21,7 @@ final class WalletTransactionsListener ) {} /** - * Generates a WalletStatement for User-received Transactions. + * Generates an income statement for User-received Transactions. */ public function processTransaction( Transaction $transaction, @@ -31,13 +31,13 @@ final class WalletTransactionsListener return; } - $statement = $this->wallet->save($transaction); + $income = $this->wallet->save($transaction); - if ($statement->getId() !== null) { + if ($income->getId() !== null) { return; } - $event->getObjectManager()->persist($statement); + $event->getObjectManager()->persist($income); $event->getObjectManager()->flush(); } } diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index 2ca2f2c..2cf8e2c 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -52,18 +52,14 @@ class WalletGateway implements GatewayInterface $transaction->setOrigin($origin); $transaction->setTarget($charge->getTarget()); - $outgoing = new WalletStatement(); - $outgoing->setTransaction($transaction); - $outgoing->setDirection(StatementDirection::Outgoing); + $expenditure = $this->wallet->spend($transaction); - $outgoing = $this->wallet->spend($transaction); + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); } $checkout = $this->checkoutService->chargeCheckout($checkout); - $this->entityManager->persist($checkout); - $this->entityManager->flush(); - return $checkout; } From 10968faf235a52b33fb3c2497dce4d9523356982 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 14:52:55 +0100 Subject: [PATCH 178/191] Calc balance only on income statements instead of skipping non income statements --- src/Gateway/Wallet/WalletService.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index 763ad12..66c8428 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -31,15 +31,11 @@ class WalletService public function getBalance(Accounting $accounting): Money { $total = Brick\Money::ofMinor(0, $accounting->getCurrency()); - $statements = $this->getStatements($accounting); + $statements = $this->statementRepository->findByTarget($accounting); foreach ($statements as $statement) { $balance = $this->money->toBrick($statement->getBalance()); - if (!$statement->hasDirection(StatementDirection::Incoming)) { - continue; - } - $total = $total->plus($balance); } From 180a9b518b28b93ade323371793cbe7fb83c827d Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 15:15:09 +0100 Subject: [PATCH 179/191] Display accounting balances --- src/ApiResource/Accounting/Accounting.php | 3 ++ src/Mapping/Accounting/AccountingMapper.php | 34 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php index 2f85039..e009954 100644 --- a/src/ApiResource/Accounting/Accounting.php +++ b/src/ApiResource/Accounting/Accounting.php @@ -6,6 +6,7 @@ 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\Money; use App\State\Accounting\AccountingStateProcessor; use App\State\Accounting\AccountingStateProvider; @@ -32,4 +33,6 @@ class Accounting public ?string $currency = null; public ?AccountingOwnerInterface $owner = null; + + public ?Money $balance = null; } diff --git a/src/Mapping/Accounting/AccountingMapper.php b/src/Mapping/Accounting/AccountingMapper.php index fac9119..ac8ef8d 100644 --- a/src/Mapping/Accounting/AccountingMapper.php +++ b/src/Mapping/Accounting/AccountingMapper.php @@ -4,7 +4,13 @@ namespace App\Mapping\Accounting; use App\ApiResource\Accounting as Resource; use App\Entity\Accounting as Entity; +use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Money; +use App\Entity\User; +use App\Gateway\Wallet\WalletService; +use App\Library\Economy\MoneyService; use App\Repository\Accounting\AccountingRepository; +use App\Repository\Accounting\TransactionRepository; use Doctrine\ORM\EntityManagerInterface; class AccountingMapper @@ -12,6 +18,9 @@ class AccountingMapper public function __construct( private EntityManagerInterface $entityManager, private AccountingRepository $accountingRepository, + private TransactionRepository $transactionRepository, + private WalletService $wallet, + private MoneyService $money, ) {} public function toResource(Entity\Accounting $entity): Resource\Accounting @@ -22,6 +31,7 @@ class AccountingMapper $resource->id = $entity->getId(); $resource->currency = $entity->getCurrency(); $resource->owner = $owner; + $resource->balance = $this->getBalance($owner); return $resource; } @@ -39,4 +49,28 @@ class AccountingMapper return $entity; } + + private function getBalance(AccountingOwnerInterface $owner): Money + { + if ($owner instanceof User) { + return $this->wallet->getBalance($owner->getAccounting()); + } + + $accounting = $owner->getAccounting(); + + $balance = new Money(0, $accounting->getCurrency()); + $transactions = $this->transactionRepository->findByAccounting($accounting); + + foreach ($transactions as $transaction) { + if ($transaction->getTarget() === $accounting) { + $balance = $this->money->add($transaction->getMoney(), $balance); + } + + if ($transaction->getOrigin() === $accounting) { + $balance = $this->money->substract($transaction->getMoney(), $balance); + } + } + + return $balance; + } } From 92f7787e7457bdc578e1e4db6bcd98d3c984de83 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 19 Nov 2024 15:15:24 +0100 Subject: [PATCH 180/191] Find transactions by accounting --- .../Accounting/TransactionRepository.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Repository/Accounting/TransactionRepository.php b/src/Repository/Accounting/TransactionRepository.php index 6da90bf..625b233 100644 --- a/src/Repository/Accounting/TransactionRepository.php +++ b/src/Repository/Accounting/TransactionRepository.php @@ -2,6 +2,7 @@ namespace App\Repository\Accounting; +use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -21,6 +22,49 @@ class TransactionRepository extends ServiceEntityRepository parent::__construct($registry, Transaction::class); } + /** + * @return Transaction[] Transactions originated from or targeting the Accounting + */ + public function findByAccounting(Accounting $accounting): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.origin = :val') + ->orWhere('t.target = :val') + ->setParameter('val', $accounting) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return Transaction[] Transactions originated from the Accounting + */ + public function findByOrigin(Accounting $origin): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.origin = :val') + ->setParameter('val', $origin) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return Transaction[] Transactions targetting the Accounting + */ + public function findByTarget(Accounting $origin): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.target = :val') + ->setParameter('val', $origin) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + // /** // * @return Transaction[] Returns an array of Transaction objects // */ From 8fb7865bbacacdaed4af48a25d3cf0e109b4107b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 10:14:36 +0100 Subject: [PATCH 181/191] Remove unnecessary statements --- src/ApiResource/Accounting/Accounting.php | 1 - src/ApiResource/Gateway/Gateway.php | 2 +- src/Entity/WalletStatement.php | 2 -- src/Gateway/Wallet/WalletGateway.php | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php index e009954..6b446bc 100644 --- a/src/ApiResource/Accounting/Accounting.php +++ b/src/ApiResource/Accounting/Accounting.php @@ -18,7 +18,6 @@ use App\State\Accounting\AccountingStateProvider; * perform corroboration of funds and store the Transactions into the system. */ #[API\ApiResource( - shortName: 'Accounting', stateOptions: new Options(entityClass: Entity\Accounting::class), provider: AccountingStateProvider::class, processor: AccountingStateProcessor::class, diff --git a/src/ApiResource/Gateway/Gateway.php b/src/ApiResource/Gateway/Gateway.php index 6f9fbd7..c896c69 100644 --- a/src/ApiResource/Gateway/Gateway.php +++ b/src/ApiResource/Gateway/Gateway.php @@ -12,7 +12,7 @@ use App\State\Gateway\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\ApiResource()] #[API\GetCollection(provider: GatewayStateProvider::class)] #[API\Get(provider: GatewayStateProvider::class)] class Gateway diff --git a/src/Entity/WalletStatement.php b/src/Entity/WalletStatement.php index e1e5a49..be15c70 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/WalletStatement.php @@ -2,7 +2,6 @@ namespace App\Entity; -use ApiPlatform\Metadata as API; use App\Entity\Accounting\Transaction; use App\Gateway\Wallet\StatementDirection; use App\Repository\WalletStatementRepository; @@ -18,7 +17,6 @@ use Doctrine\ORM\Mapping as ORM; * 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 { diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index 2cf8e2c..02f4c1f 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -5,7 +5,6 @@ namespace App\Gateway\Wallet; use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Checkout; use App\Entity\Money; -use App\Entity\WalletStatement; use App\Gateway\ChargeType; use App\Gateway\GatewayInterface; use App\Library\Economy\MoneyService; From 555f06eae31abd1deb19438bbb37340e0b4d6059 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 10:36:08 +0100 Subject: [PATCH 182/191] Use dedicated exception class for duplicate gateway names --- .../Exception/DuplicateGatewayException.php | 21 +++++++++++++++++++ src/Gateway/GatewayLocator.php | 16 +++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 src/Gateway/Exception/DuplicateGatewayException.php diff --git a/src/Gateway/Exception/DuplicateGatewayException.php b/src/Gateway/Exception/DuplicateGatewayException.php new file mode 100644 index 0000000..72ac943 --- /dev/null +++ b/src/Gateway/Exception/DuplicateGatewayException.php @@ -0,0 +1,21 @@ + */ public function getAll(): array { @@ -41,7 +42,7 @@ class GatewayLocator public function get(string $name): GatewayInterface { if (!\array_key_exists($name, $this->gatewaysByName)) { - throw new \Exception("Could not match '$name' to the name of any available Gateway implementation"); + throw new \Exception("Could not match '$name' to the name of any available Gateway"); } return $this->gatewaysByName[$name]; @@ -54,7 +55,7 @@ class GatewayLocator { $gateway = $checkout->getGatewayName(); if (!$gateway) { - throw new \Exception('The given GatewayCheckout does not specify a Gateway'); + throw new \Exception('The given Gateway Checkout does not specify a Gateway'); } return $this->get($gateway); @@ -74,14 +75,11 @@ class GatewayLocator $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", + throw new DuplicateGatewayException( $gatewayName, - $gatewayClass, - $gatewaysValidated[$gatewayName] + $gatewayClass::class, + $gatewaysValidated[$gatewayName]::class ); - - throw new \Exception($exceptionMessage); } $gatewaysValidated[$gatewayName] = $gatewayClass; From cb2e07ec017c5ba69d51e972e5fb02a3b8e9e8e1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 10:43:39 +0100 Subject: [PATCH 183/191] Use dedicated exception class for missing gateway --- src/Gateway/Exception/MissingGatewayException.php | 13 +++++++++++++ src/Gateway/GatewayLocator.php | 10 +++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 src/Gateway/Exception/MissingGatewayException.php diff --git a/src/Gateway/Exception/MissingGatewayException.php b/src/Gateway/Exception/MissingGatewayException.php new file mode 100644 index 0000000..ccd8d73 --- /dev/null +++ b/src/Gateway/Exception/MissingGatewayException.php @@ -0,0 +1,13 @@ +gatewaysByName)) { - throw new \Exception("Could not match '$name' to the name of any available Gateway"); + throw new MissingGatewayException($name); } return $this->gatewaysByName[$name]; @@ -53,12 +54,7 @@ class GatewayLocator */ public function getByCheckout(Checkout $checkout): GatewayInterface { - $gateway = $checkout->getGatewayName(); - if (!$gateway) { - throw new \Exception('The given Gateway Checkout does not specify a Gateway'); - } - - return $this->get($gateway); + return $this->get($checkout->getGatewayName()); } /** From 7ffd7353da41aeb8f9f7d8441b668281e4510abe Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 10:57:51 +0100 Subject: [PATCH 184/191] Disable single_line_throw --- .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 74082be..a7f6eb1 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -9,6 +9,7 @@ return (new PhpCsFixer\Config()) '@Symfony' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 'function_declaration' => ['closure_fn_spacing' => 'none'], + 'single_line_throw' => false, 'single_line_empty_body' => true, 'yoda_style' => [ 'equal' => false, From da56cf7c0af06d234ff59ee936be18925b3e3cb4 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 10:58:20 +0100 Subject: [PATCH 185/191] Better name for gateway by checkout getter --- src/Gateway/GatewayLocator.php | 2 +- src/Mapping/Gateway/CheckoutMapper.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gateway/GatewayLocator.php b/src/Gateway/GatewayLocator.php index 5c29f83..688ffd3 100644 --- a/src/Gateway/GatewayLocator.php +++ b/src/Gateway/GatewayLocator.php @@ -52,7 +52,7 @@ class GatewayLocator /** * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway */ - public function getByCheckout(Checkout $checkout): GatewayInterface + public function getForCheckout(Checkout $checkout): GatewayInterface { return $this->get($checkout->getGatewayName()); } diff --git a/src/Mapping/Gateway/CheckoutMapper.php b/src/Mapping/Gateway/CheckoutMapper.php index 0bd81f6..e872582 100644 --- a/src/Mapping/Gateway/CheckoutMapper.php +++ b/src/Mapping/Gateway/CheckoutMapper.php @@ -24,7 +24,7 @@ class CheckoutMapper $resource = new Resource\Checkout(); $resource->id = $entity->getId(); - $gateway = $this->gatewayLocator->getByCheckout($entity); + $gateway = $this->gatewayLocator->getForCheckout($entity); $resource->gateway = $this->gatewayMapper->toResource($gateway); $resource->origin = $this->accountingMapper->toResource($entity->getOrigin()); From 350d8b8670707c7badb58448f06abbca9fecc5f1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 11:01:22 +0100 Subject: [PATCH 186/191] Improve code in supported charge types validator --- .../SupportedChargeTypesValidator.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Validator/SupportedChargeTypesValidator.php b/src/Validator/SupportedChargeTypesValidator.php index d0398a7..eefda51 100644 --- a/src/Validator/SupportedChargeTypesValidator.php +++ b/src/Validator/SupportedChargeTypesValidator.php @@ -13,6 +13,10 @@ class SupportedChargeTypesValidator extends ConstraintValidator private GatewayLocator $gatewayLocator, ) {} + /** + * @param Collection $value + * @param SupportedChargeTypes $constraint + */ public function validate(mixed $value, Constraint $constraint): void { if ($value === null || $value === '') { @@ -23,17 +27,12 @@ class SupportedChargeTypesValidator extends ConstraintValidator return; } - /** @var Collection */ - $charges = $value; + $checkout = $value->toArray()[0]->getCheckout(); + $gateway = $this->gatewayLocator->getForCheckout($checkout); + $supportedTypes = $gateway->getSupportedChargeTypes(); - $checkout = $charges->toArray()[0]->getCheckout(); - $gateway = $this->gatewayLocator->getByCheckout($checkout); - - foreach ($charges as $charge) { - if (!\in_array( - $charge->getType(), - $gateway->getSupportedChargeTypes() - )) { + foreach ($value as $charge) { + if (!\in_array($charge->getType(), $supportedTypes)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $charge->getType()->value) ->setParameter('{{ gateway }}', $gateway->getName()) From 42fccbe0355b416871a69842fb495f893d4455cf Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 11:03:11 +0100 Subject: [PATCH 187/191] Align var name in docblock --- src/Validator/SupportedChargeTypesValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validator/SupportedChargeTypesValidator.php b/src/Validator/SupportedChargeTypesValidator.php index eefda51..8851433 100644 --- a/src/Validator/SupportedChargeTypesValidator.php +++ b/src/Validator/SupportedChargeTypesValidator.php @@ -15,7 +15,7 @@ class SupportedChargeTypesValidator extends ConstraintValidator /** * @param Collection $value - * @param SupportedChargeTypes $constraint + * @param SupportedChargeTypes $constraint */ public function validate(mixed $value, Constraint $constraint): void { From c867cd5fe3ee1239bd58e893251bb04d47d85bb9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 12:28:12 +0100 Subject: [PATCH 188/191] Improve Project api test case coverage --- tests/Entity/ProjectApiTest.php | 51 ++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index e5579be..e2d8055 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -3,6 +3,10 @@ namespace App\Tests\Entity; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use App\Entity\Project; +use App\Entity\ProjectStatus; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Test\ResetDatabase; @@ -10,25 +14,64 @@ class ProjectApiTest extends ApiTestCase { use ResetDatabase; + private EntityManagerInterface $entityManager; + + public function setUp(): void + { + self::bootKernel(); + + $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); + } + public function testGetCollection(): void { static::createClient()->request('GET', '/v4/projects'); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['@id' => '/v4/projects']); + $this->assertJsonContains(['@type' => 'hydra:Collection']); + $this->assertJsonContains(['hydra:totalItems' => 0]); $this->assertJsonContains(['hydra:member' => []]); + + $owner = new User(); + $owner->setUsername('test_user'); + $owner->setEmail('testuser@example.com'); + $owner->setPassword('projectapitestuserpassword'); + + $project = new Project(); + $project->setTitle('Test Project'); + $project->setOwner($owner); + $project->setStatus(ProjectStatus::Editing); + + $this->entityManager->persist($owner); + $this->entityManager->persist($project); + $this->entityManager->flush(); + + static::createClient()->request('GET', '/v4/projects'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['hydra:totalItems' => 1]); + $this->assertJsonContains(['hydra:member' => [ + [ + 'title' => 'Test Project', + 'accounting' => [], + 'status' => ProjectStatus::Editing->value + ] + ]]); } - public function testPostUnauthorized(): void + public function testPostUnauthorized() { $client = static::createClient(); $client->request( 'POST', '/v4/projects', - ['json' => [ - 'title' => 'ProjectApiTest Project', - ]] + [ + 'json' => [ + 'title' => 'ProjectApiTest Project', + ] + ] ); $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); From ec46c7871addf6e5ba9e1a9f9e8c717bfccc7191 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 12:40:55 +0100 Subject: [PATCH 189/191] Improve thorougness of accounting entity test --- tests/Entity/Accounting/AccountingTest.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/Entity/Accounting/AccountingTest.php b/tests/Entity/Accounting/AccountingTest.php index 6d7f8e3..2792f4a 100644 --- a/tests/Entity/Accounting/AccountingTest.php +++ b/tests/Entity/Accounting/AccountingTest.php @@ -38,8 +38,13 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingA->getId()); + + $this->assertSame($tipjarA, $accountingA->getOwner()); + $this->assertSame($tipjarA, $accountingA->getTipjar()); $this->assertEquals($tipjarA->getId(), $accountingA->getOwner()->getId()); - $this->assertEquals(Tipjar::class, $accountingA->getOwner()::class); + + $this->assertEquals(null, $accountingA->getUser()); + $this->assertEquals(null, $accountingA->getProject()); $tipjarB = new Tipjar(); $tipjarB->setName('TEST_TIPJAR_B'); @@ -56,7 +61,16 @@ class AccountingTest extends KernelTestCase ->getAccounting(); $this->assertNotNull($accountingB->getId()); + + $this->assertSame($tipjarB, $accountingB->getOwner()); + $this->assertNotSame($tipjarA, $accountingB->getOwner()); + $this->assertSame($tipjarB, $accountingB->getTipjar()); + $this->assertNotSame($tipjarA, $accountingB->getTipjar()); + $this->assertEquals($tipjarB->getId(), $accountingB->getOwner()->getId()); - $this->assertEquals(Tipjar::class, $accountingB->getOwner()::class); + $this->assertNotEquals($tipjarA->getId(), $accountingB->getOwner()->getId()); + + $this->assertEquals(null, $accountingB->getUser()); + $this->assertEquals(null, $accountingB->getProject()); } } From 270533b194e001b5e58acb49932e5dbdd2ce38e1 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 13:05:14 +0100 Subject: [PATCH 190/191] Add tests for money additon and substraction --- tests/Library/Economy/MoneyServiceTest.php | 55 +++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php index 949f4ca..2ad6b97 100644 --- a/tests/Library/Economy/MoneyServiceTest.php +++ b/tests/Library/Economy/MoneyServiceTest.php @@ -28,16 +28,14 @@ class MoneyServiceTest extends KernelTestCase public function testComparesLess() { - $a = new Money(300, 'EUR'); - $b = new Money(200, 'EUR'); + $more = new Money(300, 'EUR'); + $less = new Money(200, 'EUR'); - $this->assertTrue($this->moneyService->isLess($b, $a)); - $this->assertFalse($this->moneyService->isLess($a, $b)); - - $c = new Money(100, 'JPY'); + $this->assertTrue($this->moneyService->isLess($less, $more)); + $this->assertFalse($this->moneyService->isLess($more, $less)); // We are cooked if this fails and I'm not talking about code - $this->assertTrue($this->moneyService->isLess($c, $a)); + $this->assertTrue($this->moneyService->isLess(new Money(100, 'JPY'), $more)); } public function testComparesMore() @@ -58,4 +56,47 @@ class MoneyServiceTest extends KernelTestCase $this->assertFalse($this->moneyService->isMoreOrSame($d, $c)); $this->assertFalse($this->moneyService->isMoreOrSame($d, $a)); } + + public function testAddition() + { + $a = new Money(100, 'EUR'); + $b = new Money(110, 'EUR'); + + $c = $this->moneyService->add($a, $b); + + $this->assertEquals(100, $a->amount); + $this->assertEquals(110, $b->amount); + $this->assertEquals(210, $c->amount); + + $d = new Money(100, 'USD'); + $e = $this->moneyService->add($a, $d); + + $this->assertEquals($d->amount, $a->amount); + $this->assertEquals($d->currency, $e->currency); + + $this->assertNotEquals($d->amount, $e->amount); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $d)); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); + } + + public function testSubstraction() + { + $a = new Money(101, 'EUR'); + $b = new Money(210, 'EUR'); + + $c = $this->moneyService->substract($a, $b); + + $this->assertEquals(101, $a->amount); + $this->assertEquals(210, $b->amount); + $this->assertEquals(109, $c->amount); + + $d = new Money(500, 'USD'); + $e = $this->moneyService->substract($a, $d); + + $this->assertEquals($d->currency, $e->currency); + + $this->assertNotEquals($d->amount, $e->amount); + $this->assertTrue($this->moneyService->isLess($e, $d)); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); + } } From 83c6d62df6c5e47bdea612c0204a28b4401227df Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 20 Nov 2024 14:11:19 +0100 Subject: [PATCH 191/191] Add trailing commas --- tests/Entity/ProjectApiTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index e2d8055..ab1978f 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -55,8 +55,8 @@ class ProjectApiTest extends ApiTestCase [ 'title' => 'Test Project', 'accounting' => [], - 'status' => ProjectStatus::Editing->value - ] + 'status' => ProjectStatus::Editing->value, + ], ]]); } @@ -70,7 +70,7 @@ class ProjectApiTest extends ApiTestCase [ 'json' => [ 'title' => 'ProjectApiTest Project', - ] + ], ] );