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/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b8c750c..74082be 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,6 +8,8 @@ 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, 'identical' => false, 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" } 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/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 diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php new file mode 100644 index 0000000..2f85039 --- /dev/null +++ b/src/ApiResource/Accounting/Accounting.php @@ -0,0 +1,35 @@ +gateway->getName(); - } + public ?string $name = null; + + /** + * @var array + */ + public ?array $supports = null; } diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php index b94e121..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; @@ -24,8 +24,7 @@ class Version public function __construct( private readonly LogEntry $log, private readonly object $entity, - ) { - } + ) {} /** * The ID of the version record. @@ -56,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/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 4b6ac86..5fe6179 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -50,9 +50,9 @@ class BenzinaPumpCommand extends Command $this->addOption( 'skip-pumped', null, - InputOption::VALUE_OPTIONAL, + 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/Command/GatewaysSetupCommand.php b/src/Command/GatewaysSetupCommand.php deleted file mode 100644 index b9bf75c..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/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..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; @@ -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/Compiler/GatewaysCompilerPass.php b/src/DependencyInjection/Compiler/GatewaysCompilerPass.php deleted file mode 100755 index ad0d23b..0000000 --- a/src/DependencyInjection/Compiler/GatewaysCompilerPass.php +++ /dev/null @@ -1,158 +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 with the same name - * - * @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations - */ - 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 - { - $classesDir = join(DIRECTORY_SEPARATOR, [ - $container->getParameter('kernel.project_dir'), - 'src', - ...\array_slice(explode('\\', self::getGatewaysNamespace()), 1), - ]); - - $gatewayClasses = self::getGatewayClasses($classesDir); - self::validateGatewayNames($gatewayClasses); - - $gatewayNames = $this->getGatewayNames($gatewayClasses); - self::compileGatewayNames($gatewayNames); - } -} 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/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/Entity/Accounting.php b/src/Entity/Accounting.php deleted file mode 100644 index 4517e11..0000000 --- a/src/Entity/Accounting.php +++ /dev/null @@ -1,236 +0,0 @@ - - */ - #[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; - - #[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() - { - /* - * TO-DO: This property must be loaded from App's configuration, - * ideally a configuration that can be updated via a frontend, not env var only - */ - $this->currency = 'EUR'; - $this->transactionsIssued = new ArrayCollection(); - $this->transactionsReceived = new ArrayCollection(); - } - - public function getId(): ?int - { - return $this->id; - } - - public function getCurrency(): ?string - { - return $this->currency; - } - - public function setCurrency(string $currency): static - { - $this->currency = $currency; - - 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; - } - - public 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.'); - } - - $this->ownerClass = $ownerClass; - - return $this; - } - - public function getOwnerResource(): string - { - 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; - - return $this; - } -} diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php new file mode 100644 index 0000000..9bb5665 --- /dev/null +++ b/src/Entity/Accounting/Accounting.php @@ -0,0 +1,167 @@ +currency = 'EUR'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCurrency(): ?string + { + return $this->currency; + } + + public function setCurrency(string $currency): static + { + $this->currency = $currency; + + return $this; + } + + public function getOwner(): AccountingOwnerInterface + { + $owner = $this->owner; + + return $this->$owner; + } + + public function setOwner(AccountingOwnerInterface $owner): static + { + 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/AccountingTransaction.php b/src/Entity/Accounting/Transaction.php similarity index 80% rename from src/Entity/AccountingTransaction.php rename to src/Entity/Accounting/Transaction.php index 6cc43a6..eddf3a0 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -1,9 +1,10 @@ + */ + #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] + private Collection $transactions; + + public function __construct() + { + $this->transactions = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCheckout(): ?Checkout + { + return $this->checkout; + } + + public function setCheckout(?Checkout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } + + public function getType(): ?ChargeType + { + return $this->type; + } + + public function setType(ChargeType $type): static + { + $this->type = $type; + + return $this; + } + + public function getTarget(): ?Accounting + { + return $this->target; + } + + public function setTarget(?Accounting $target): static + { + $this->target = $target; + + return $this; + } + + public function getMoney(): ?Money + { + return $this->money; + } + + public function setMoney(Money $money): static + { + $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; + } + + /** + * @return Collection + */ + public function getTransactions(): Collection + { + return $this->transactions; + } + + public function addTransaction(Transaction $transaction): static + { + if (!$this->transactions->contains($transaction)) { + $this->transactions->add($transaction); + } + + return $this; + } + + public function removeTransaction(Transaction $transaction): static + { + $this->transactions->removeElement($transaction); + + return $this; + } +} diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php new file mode 100644 index 0000000..6add15e --- /dev/null +++ b/src/Entity/Gateway/Checkout.php @@ -0,0 +1,257 @@ + + */ + #[Assert\NotBlank()] + #[Assert\Count(min: 1)] + #[SupportedChargeTypes()] + #[ORM\OneToMany(mappedBy: 'checkout', targetEntity: Charge::class, cascade: ['persist'])] + private Collection $charges; + + /** + * The name of the Gateway implementation to checkout with. + */ + #[Assert\NotBlank()] + #[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 Link[] + */ + #[ORM\Column] + private array $links = []; + + /** + * A list of tracking codes provided by the Gateway for this checkout.\ + * e.g: Order ID, Payment Capture ID, Checkout Session Token. + * + * @var Tracking[] + */ + #[ORM\Column] + private array $trackings = []; + + public function __construct() + { + $this->status = CheckoutStatus::Pending; + $this->charges = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOrigin(): ?Accounting + { + return $this->origin; + } + + public function setOrigin(?Accounting $origin): static + { + $this->origin = $origin; + + return $this; + } + + public function getStatus(): ?CheckoutStatus + { + return $this->status; + } + + public function setStatus(CheckoutStatus $status): static + { + $this->status = $status; + + return $this; + } + + /** + * @return Collection + */ + public function getCharges(): Collection + { + 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)) { + $this->charges->add($charge); + $charge->setCheckout($this); + } + + return $this; + } + + public function removeCharge(Charge $charge): static + { + if ($this->charges->removeElement($charge)) { + // set the owning side to null (unless already changed) + if ($charge->getCheckout() === $this) { + $charge->setCheckout(null); + } + } + + return $this; + } + + public function getGateway(): ?string + { + return $this->gateway; + } + + public function setGateway(string $gateway): static + { + $this->gateway = $gateway; + + return $this; + } + + /** + * @return Link[] + */ + public function getLinks(): array + { + 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]; + + return $this; + } + + public function removeLink(Link $link): static + { + $this->links = \array_filter( + $this->links, + function (Link $existingLink) use ($link) { + return $existingLink->href !== $link->href; + } + ); + + return $this; + } + + /** + * @return Tracking[] + */ + public function getTrackings(): array + { + 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]; + + return $this; + } + + public function removeTracking(Tracking $tracking): static + { + $this->trackings = \array_filter( + $this->trackings, + function (Tracking $existingTracking) use ($tracking) { + return $existingTracking->title !== $tracking->title + && $existingTracking->value !== $tracking->value; + } + ); + + return $this; + } +} diff --git a/src/Entity/GatewayCharge.php b/src/Entity/GatewayCharge.php deleted file mode 100644 index c78735f..0000000 --- a/src/Entity/GatewayCharge.php +++ /dev/null @@ -1,106 +0,0 @@ -id; - } - - public function getType(): ?GatewayChargeType - { - return $this->type; - } - - public function setType(GatewayChargeType $type): static - { - $this->type = $type; - - 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; - } - - public function setTarget(?Accounting $target): static - { - $this->target = $target; - - return $this; - } - - public function getTransaction(): ?AccountingTransaction - { - return $this->transaction; - } - - public function setTransaction(?AccountingTransaction $transaction): static - { - $this->transaction = $transaction; - - return $this; - } -} diff --git a/src/Entity/GatewayCheckout.php b/src/Entity/GatewayCheckout.php deleted file mode 100644 index d483ce7..0000000 --- a/src/Entity/GatewayCheckout.php +++ /dev/null @@ -1,276 +0,0 @@ - 'exact', 'charges.target' => 'exact'])] -#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)] -#[ORM\Index(fields: ['migratedId'])] -class GatewayCheckout -{ - use TimestampableCreationEntity; - use TimestampableUpdationEntity; - - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - private ?int $id = null; - - /** - * The Accounting that will issue the Transactions of the GatewayCharges after a successful checkout. - */ - #[Assert\NotBlank()] - #[ORM\ManyToOne] - #[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. - */ - #[Gedmo\Versioned] - #[API\ApiProperty(writable: false)] - #[API\ApiFilter(SearchFilter::class)] - #[ORM\Column()] - private ?GatewayCheckoutStatus $status = null; - - /** - * The name of the Gateway implementation to checkout with. - */ - #[GatewayName] - #[Assert\NotBlank()] - #[API\ApiFilter(SearchFilter::class)] - #[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 GatewayLink[] - */ - #[API\ApiProperty(writable: false)] - #[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 GatewayTracking[] - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column] - private array $gatewayTrackings = []; - - /** - * 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; - - /** - * A free-form collection of additional data associated with this checkout operation. - */ - #[ORM\Column(nullable: true)] - private ?array $metadata = null; - - public function __construct() - { - $this->charges = new ArrayCollection(); - $this->status = GatewayCheckoutStatus::Pending; - } - - public function getId(): ?int - { - return $this->id; - } - - public function getOrigin(): ?Accounting - { - return $this->origin; - } - - public function setOrigin(?Accounting $origin): static - { - $this->origin = $origin; - - return $this; - } - - /** - * @return Collection - */ - public function getCharges(): Collection - { - return $this->charges; - } - - public function addCharge(GatewayCharge $charge): static - { - if (!$this->charges->contains($charge)) { - $this->charges->add($charge); - } - - return $this; - } - - 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; - - return $this; - } - - public function getGateway(): ?string - { - return $this->gateway; - } - - public function setGateway(string $gateway): static - { - $this->gateway = $gateway; - - return $this; - } - - /** - * @return GatewayLink[] - */ - public function getGatewayLinks(): array - { - return $this->gatewayLinks; - } - - public function addGatewayLink(GatewayLink $link): static - { - $this->gatewayLinks = [...$this->gatewayLinks, $link]; - - return $this; - } - - public function removeGatewayLink(GatewayLink $link): static - { - $this->gatewayLinks = \array_filter( - $this->gatewayLinks, - function (GatewayLink $existingLink) use ($link) { - return $existingLink->href !== $link->href; - } - ); - - return $this; - } - - /** - * @return GatewayTracking[] - */ - public function getGatewayTrackings(): array - { - return $this->gatewayTrackings; - } - - public function addGatewayTracking(GatewayTracking $tracking): static - { - $this->gatewayTrackings = [...$this->gatewayTrackings, $tracking]; - - return $this; - } - - public function removeGatewayTracking(GatewayTracking $tracking): static - { - $this->gatewayTrackings = \array_filter( - $this->gatewayTrackings, - function (GatewayTracking $existingTracking) use ($tracking) { - return $existingTracking->title !== $tracking->title - && $existingTracking->value !== $tracking->value; - } - ); - - 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; - } - - public function getMetadata(): ?array - { - return $this->metadata; - } - - public function setMetadata(?array $metadata): static - { - $this->metadata = $metadata; - - return $this; - } -} diff --git a/src/Entity/Interface/AccountingOwnerInterface.php b/src/Entity/Interface/AccountingOwnerInterface.php new file mode 100644 index 0000000..35da0b2 --- /dev/null +++ b/src/Entity/Interface/AccountingOwnerInterface.php @@ -0,0 +1,14 @@ + 'exact', ])] #[ORM\Entity(repositoryClass: ProjectRepository::class)] -class Project +class Project implements AccountingOwnerInterface { - use TimestampableCreationEntity; - use TimestampableUpdationEntity; + use MigratedEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] @@ -44,8 +48,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(inversedBy: 'project', cascade: ['persist'])] private ?Accounting $accounting = null; /** @@ -64,23 +67,12 @@ class Project #[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; + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; } public function getId(): ?int @@ -105,7 +97,7 @@ class Project return $this->accounting; } - public function setAccounting(Accounting $accounting): static + public function setAccounting(?Accounting $accounting): static { $this->accounting = $accounting; @@ -135,28 +127,4 @@ class Project 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/Tipjar.php b/src/Entity/Tipjar.php index 2f0a00f..621bd84 100644 --- a/src/Entity/Tipjar.php +++ b/src/Entity/Tipjar.php @@ -3,6 +3,8 @@ namespace App\Entity; use ApiPlatform\Metadata as API; +use App\Entity\Accounting\Accounting; +use App\Entity\Interface\AccountingOwnerInterface; use App\Repository\TipjarRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -19,7 +21,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,10 +35,17 @@ class Tipjar private ?string $name = null; #[API\ApiProperty(writable: false)] - #[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] + #[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; @@ -59,7 +68,7 @@ class Tipjar return $this->accounting; } - public function setAccounting(Accounting $accounting): static + public function setAccounting(?Accounting $accounting): static { $this->accounting = $accounting; 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/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 078978b..35d74b8 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,9 +3,12 @@ namespace App\Entity; 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\MigratedEntity; +use App\Entity\Trait\TimestampedCreationEntity; +use App\Entity\Trait\TimestampedUpdationEntity; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; use App\Repository\UserRepository; @@ -38,10 +41,11 @@ 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; + use MigratedEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] @@ -95,8 +99,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(inversedBy: 'user', cascade: ['persist'])] private ?Accounting $accounting = null; /** @@ -125,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. */ @@ -151,7 +140,13 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs public function __construct() { - $this->migrated = false; + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; + + $this->emailConfirmed = false; + $this->active = false; $this->tokens = new ArrayCollection(); $this->projects = new ArrayCollection(); @@ -289,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; @@ -362,30 +357,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 */ 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] 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 new file mode 100644 index 0000000..08f0ac9 --- /dev/null +++ b/src/Entity/WalletStatement.php @@ -0,0 +1,179 @@ + + */ + #[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, cascade: ['persist'])] + private Collection $financedBy; + + public function __construct() + { + $this->financesTo = new ArrayCollection(); + $this->financedBy = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTransaction(): ?Transaction + { + return $this->transaction; + } + + public function setTransaction(Transaction $transaction): static + { + $this->transaction = $transaction; + + return $this; + } + + public function getDirection(): ?StatementDirection + { + return $this->direction; + } + + public function hasDirection(StatementDirection $direction): bool + { + return $this->direction === $direction; + } + + public function setDirection(StatementDirection $direction): static + { + $this->direction = $direction; + + return $this; + } + + public function getBalance(): ?Money + { + return $this->balance; + } + + public function setBalance(Money $balance): static + { + $this->balance = $balance; + + return $this; + } + + /** + * @return Collection + */ + public function getFinancesTo(): Collection + { + return $this->financesTo; + } + + public function addFinancesTo(WalletFinancement $financesTo): static + { + if (!$this->financesTo->contains($financesTo)) { + $this->financesTo->add($financesTo); + $financesTo->setOrigin($this); + } + + return $this; + } + + public function removeFinancesTo(WalletFinancement $financesTo): static + { + 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/EventListener/WalletTransactionsListener.php b/src/EventListener/WalletTransactionsListener.php new file mode 100644 index 0000000..f868e65 --- /dev/null +++ b/src/EventListener/WalletTransactionsListener.php @@ -0,0 +1,38 @@ +getTarget(); + + if ($target->getOwner() instanceof User) { + $this->wallet->save($transaction); + } + } +} diff --git a/src/Entity/GatewayChargeType.php b/src/Gateway/ChargeType.php similarity index 78% rename from src/Entity/GatewayChargeType.php rename to src/Gateway/ChargeType.php index 84c7ba1..15725d1 100644 --- a/src/Entity/GatewayChargeType.php +++ b/src/Gateway/ChargeType.php @@ -1,8 +1,8 @@ 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 GatewaysCompilerPass::compileGatewayNames() - */ - public static function getNamesStatic(): array - { - return explode(PHP_EOL, \file_get_contents(GatewaysCompilerPass::getLockFile())); - } - /** * @return GatewayInterface[] */ @@ -77,7 +66,7 @@ class GatewayLocator /** * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway */ - public function getGatewayOf(GatewayCheckout $checkout): GatewayInterface + public function getGatewayOf(Checkout $checkout): GatewayInterface { $gateway = $checkout->getGateway(); if (!$gateway) { @@ -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; + } + } } diff --git a/src/Entity/GatewayLink.php b/src/Gateway/Link.php similarity index 90% rename from src/Entity/GatewayLink.php rename to src/Gateway/Link.php index 6576424..40cb124 100644 --- a/src/Entity/GatewayLink.php +++ b/src/Gateway/Link.php @@ -1,8 +1,8 @@ paypal->postOrder([ 'intent' => self::PAYPAL_ORDER_INTENT, @@ -65,24 +71,24 @@ class PaypalGateway implements GatewayInterface 'payment_source' => $this->getPaypalPaymentSource($checkout), ]); - $tracking = new GatewayTracking(); - $tracking->setValue($order['id']); - $tracking->setTitle(self::TRACKING_TITLE_ORDER); + $tracking = new Tracking(); + $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']) - ? GatewayLinkType::Payment - : GatewayLinkType::Debug; + ? LinkType::Payment + : LinkType::Debug; - $link = new GatewayLink(); - $link->setHref($linkData['href']); - $link->setRel($linkData['rel']); - $link->setMethod($linkData['method']); - $link->setType($linkType); + $link = new Link(); + $link->href = $linkData['href']; + $link->rel = $linkData['rel']; + $link->method = $linkData['method']; + $link->type = $linkType; - $checkout->addGatewayLink($link); + $checkout->addLink($link); } return $checkout; @@ -90,7 +96,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.')); } @@ -98,10 +104,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)); } @@ -121,15 +127,18 @@ 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 = new Tracking(); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $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)); } @@ -164,21 +173,21 @@ 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->setValue($purchaseUnit['payments']['captures'][0]['id']); - $tracking->setTitle(self::TRACKING_TITLE_TRANSACTION); + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $checkout = $this->checkoutService->chargeCheckout($checkout); } - private function getPaypalMoney(GatewayCharge $charge): array + private function getPaypalMoney(Charge $charge): array { $brick = BrickMoney::ofMinor( $charge->getMoney()->amount, @@ -191,21 +200,21 @@ class PaypalGateway implements GatewayInterface ]; } - private function getPaypalPurchaseUnits(GatewayCheckout $checkout): array + private function getPaypalPurchaseUnits(Checkout $checkout): array { $units = []; 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, 'custom_id' => $reference, 'items' => [ [ - 'name' => $charge::MESSAGE_STATEMENT, - 'description' => $charge::MESSAGE_STATEMENT, + 'name' => $charge->getTitle(), + 'description' => $charge->getDescription(), 'quantity' => '1', 'unit_amount' => [ ...$money, @@ -226,7 +235,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/PaypalGatewayService.php b/src/Gateway/Paypal/PaypalService.php similarity index 94% rename from src/Library/Economy/Payment/PaypalGatewayService.php rename to src/Gateway/Paypal/PaypalService.php index 7b04d99..e826d05 100644 --- a/src/Library/Economy/Payment/PaypalGatewayService.php +++ b/src/Gateway/Paypal/PaypalService.php @@ -1,6 +1,6 @@ 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']); diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php similarity index 63% rename from src/Library/Economy/Payment/StripeGateway.php rename to src/Gateway/Stripe/StripeGateway.php index 0874cbe..dc69356 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -1,16 +1,18 @@ 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} @@ -57,20 +65,20 @@ class StripeGateway implements GatewayInterface 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)), ]); - $link = new GatewayLink(); + $link = new Link(); - $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 = LinkType::Payment; - $checkout->addGatewayLink($link); + $checkout->addLink($link); - $tracking = new GatewayTracking(); - $tracking->setValue($session->id); - $tracking->setTitle(self::TRACKING_TITLE_CHECKOUT); + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_CHECKOUT; + $tracking->value = $session->id; - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); return $checkout; } @@ -83,14 +91,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; } @@ -100,6 +108,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)); } @@ -121,10 +132,20 @@ class StripeGateway implements GatewayInterface } } - private function getStripeMode(GatewayCheckout $checkout): string + private function getStripeCustomer(Checkout $checkout): string + { + $owner = $checkout->getOrigin()->getOwner(); + if (!$owner instanceof User) { + return ''; + } + + return $owner->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; } } @@ -132,7 +153,7 @@ class StripeGateway implements GatewayInterface return StripeSession::MODE_PAYMENT; } - private function getStripeLineItems(GatewayCheckout $checkout): array + private function getStripeLineItems(Checkout $checkout): array { $items = []; @@ -140,13 +161,13 @@ class StripeGateway implements GatewayInterface $price = [ 'currency' => $charge->getMoney()->currency, 'unit_amount' => $charge->getMoney()->amount, - 'product_data' => [ - 'name' => $charge::MESSAGE_STATEMENT, - 'statement_descriptor' => $charge::MESSAGE_STATEMENT, - ], + 'product_data' => \array_filter([ + 'name' => $charge->getTitle(), + 'statement_descriptor' => $charge->getDescription(), + ], fn($v) => $v !== null), ]; - if ($charge->getType() === GatewayChargeType::Recurring) { + if ($charge->getType() === ChargeType::Recurring) { $price['recurring'] = ['interval' => 'month']; } diff --git a/src/Entity/GatewayTracking.php b/src/Gateway/Tracking.php similarity index 87% rename from src/Entity/GatewayTracking.php rename to src/Gateway/Tracking.php index b702411..69c7226 100644 --- a/src/Entity/GatewayTracking.php +++ b/src/Gateway/Tracking.php @@ -1,10 +1,10 @@ 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 Transaction(); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($origin); + $transaction->setTarget($charge->getTarget()); + + $outgoing = new WalletStatement(); + $outgoing->setTransaction($transaction); + $outgoing->setDirection(StatementDirection::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(Checkout $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; + } +} diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php new file mode 100644 index 0000000..b64f153 --- /dev/null +++ b/src/Gateway/Wallet/WalletService.php @@ -0,0 +1,130 @@ +walletStatementRepository->findByAccounting($accounting); + } + + public function getBalance(Accounting $accounting): Money + { + $total = Brick\Money::ofMinor(0, $accounting->getCurrency()); + $statements = $this->getStatements($accounting); + + foreach ($statements as $statement) { + $balance = $this->money->toBrick($statement->getBalance()); + + if (!$statement->hasDirection(StatementDirection::Incoming)) { + continue; + } + + $total = $total->plus($balance); + } + + return $this->money->toMoney($total); + } + + /** + * Puts the money of a Transaction into the target wallet. + * + * @param Transaction $transaction The Transaction targetting a wallet + * + * @return WalletStatement An incoming statement for the target wallet + */ + public function save(Transaction $transaction): WalletStatement + { + $statement = new WalletStatement(); + $statement->setTransaction($transaction); + $statement->setDirection(StatementDirection::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 Transaction $transaction The Transaction originating from a wallet + * + * @return WalletStatement An outgoing statement financed by previous incoming statements + */ + public function spend(Transaction $transaction): WalletStatement + { + $origin = $transaction->getOrigin(); + + $spendGoal = $transaction->getMoney(); + $spentTotal = new Money(0, $spendGoal->currency); + + $outgoing = new WalletStatement(); + $outgoing->setTransaction($transaction); + $outgoing->setDirection(StatementDirection::Outgoing); + + $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) { + continue; + } + + if ($this->money->isLess($spendGoal, $balance)) { + $balanceSpent = $spendGoal; + } else { + $balanceSpent = $balance; + } + + $financement = new WalletFinancement(); + $financement->setMoney($balanceSpent); + + $incoming->setBalance($this->money->substract($balanceSpent, $balance)); + $incoming->addFinancesTo($financement); + + $outgoing->setBalance($this->money->add($balanceSpent, $spentTotal)); + $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/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 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/CheckoutsPump.php b/src/Library/Benzina/Pump/CheckoutsPump.php index 3d50f4d..e7d461e 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -2,26 +2,25 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; -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\Checkout; 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\Paypal\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; -use App\Service\GatewayCheckoutService; +use App\Service\Gateway\CheckoutService; use Doctrine\ORM\EntityManagerInterface; class CheckoutsPump extends AbstractPump implements PumpInterface @@ -34,6 +33,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; @@ -75,9 +78,8 @@ 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 { @@ -90,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(); @@ -112,13 +114,13 @@ 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)); foreach ($this->getCheckoutTrackings($record) as $tracking) { - $checkout->addGatewayTracking($tracking); + $checkout->addTracking($tracking); } $checkout->setMigrated(true); @@ -127,32 +129,35 @@ 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'])); 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()); } $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()); $checkout->addCharge($tip); } - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { + if ($checkout->getStatus() === CheckoutStatus::Charged) { $checkout = $this->checkoutService->chargeCheckout($checkout); } @@ -208,23 +213,19 @@ 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; } - 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 @@ -238,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 @@ -268,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']; @@ -287,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']; @@ -295,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 a93870c..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; @@ -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 { @@ -133,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/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. 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; } diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index 14485b5..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; @@ -52,8 +51,7 @@ class UsersPump extends AbstractPump implements PumpInterface public function __construct( private EntityManagerInterface $entityManager, - ) { - } + ) {} public function supports(mixed $batch): bool { @@ -69,8 +67,12 @@ 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->setAccounting(new Accounting()); $user->setUsername($this->getUsername($record)); $user->setPassword($record['password'] ?? ''); $user->setEmail($record['email']); @@ -79,12 +81,10 @@ class UsersPump extends AbstractPump implements PumpInterface $user->setActive(false); $user->setMigrated(true); $user->setMigratedId($record['id']); - - $accounting = new Accounting(); - $accounting->setUser($user); + $user->setDateCreated($created); + $user->setDateUpdated(new \DateTime()); $this->entityManager->persist($user); - $this->entityManager->persist($accounting); } $this->entityManager->flush(); 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; } diff --git a/src/Library/Economy/MoneyService.php b/src/Library/Economy/MoneyService.php new file mode 100644 index 0000000..c6649fc --- /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 if one Money is of less value than other. + * + * @return bool `true` if `$money` is less than `$than` + */ + public function isLess(Money $money, Money $than): bool + { + $money = $this->convert($money, $than->currency); + + return $money->isLessThan(self::toBrick($than)); + } + + /** + * Compares if one Money is of greater or equal value than other. + * + * @return bool `true` if `$money` is more than or same as `$than` + */ + public function isMoreOrSame(Money $money, Money $than): bool + { + $money = $this->convert($money, $than->currency); + + return $money->isGreaterThanOrEqualTo(self::toBrick($than)); + } + + private function convert(Money $money, string $toCurrency): Brick\Money + { + $fromCurrency = $money->currency; + if ($fromCurrency === $toCurrency) { + return self::toBrick($money); + } + + $exchange = $this->exchangeLocator->getExchangeFor($fromCurrency, $toCurrency); + + return self::toBrick($exchange->convert(self::toBrick($money), $toCurrency)); + } +} diff --git a/src/Library/Economy/Payment/WalletGateway.php b/src/Library/Economy/Payment/WalletGateway.php deleted file mode 100644 index f8380bb..0000000 --- a/src/Library/Economy/Payment/WalletGateway.php +++ /dev/null @@ -1,11 +0,0 @@ -getOwner(); + + $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/Mapping/Gateway/ChargeMapper.php b/src/Mapping/Gateway/ChargeMapper.php new file mode 100644 index 0000000..d74fc63 --- /dev/null +++ b/src/Mapping/Gateway/ChargeMapper.php @@ -0,0 +1,46 @@ +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; + } + + 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; + } +} 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/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/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/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/Gateway/CheckoutRepository.php b/src/Repository/Gateway/CheckoutRepository.php new file mode 100644 index 0000000..5a587c2 --- /dev/null +++ b/src/Repository/Gateway/CheckoutRepository.php @@ -0,0 +1,57 @@ + + * + * @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 CheckoutRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Checkout::class); + } + + /** + * @return Checkout|null + */ + public function findOneByTracking(string $title, string $value): ?array + { + // TO-DO: Use a JSON lookup into the 'trackings' column to see if it contains title and value + return null; + } + + // /** + // * @return Checkout[] Returns an array of Checkout 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): ?Checkout + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/GatewayCheckoutRepository.php b/src/Repository/GatewayCheckoutRepository.php deleted file mode 100644 index 9d8ee48..0000000 --- a/src/Repository/GatewayCheckoutRepository.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * @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) - */ -class GatewayCheckoutRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, GatewayCheckout::class); - } - - /** - * @return GatewayCheckout|null - */ - 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() - ; - } - - // /** - // * @return GatewayCheckout[] Returns an array of GatewayCheckout 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): ?GatewayCheckout - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } -} 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/WalletFinancementRepository.php b/src/Repository/WalletFinancementRepository.php new file mode 100644 index 0000000..0704d8c --- /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() + // ; + // } +} diff --git a/src/Repository/WalletStatementRepository.php b/src/Repository/WalletStatementRepository.php new file mode 100644 index 0000000..8987513 --- /dev/null +++ b/src/Repository/WalletStatementRepository.php @@ -0,0 +1,91 @@ + + */ +class WalletStatementRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + 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[] + */ + 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() + // ; + // } +} 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/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/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. 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/Gateway/CheckoutService.php similarity index 68% rename from src/Service/GatewayCheckoutService.php rename to src/Service/Gateway/CheckoutService.php index 54d049b..2ad5ac7 100644 --- a/src/Service/GatewayCheckoutService.php +++ b/src/Service/Gateway/CheckoutService.php @@ -1,23 +1,22 @@ 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()); - $charge->setTransaction($transaction); + $charge->addTransaction($transaction); } return $checkout; diff --git a/src/State/Accounting/AccountingStateProcessor.php b/src/State/Accounting/AccountingStateProcessor.php new file mode 100644 index 0000000..12fc107 --- /dev/null +++ b/src/State/Accounting/AccountingStateProcessor.php @@ -0,0 +1,35 @@ +accountingMapper->toEntity($data); + + $this->persistProcessor->process($entity, $operation, $uriVariables, $context); + + return $this->accountingMapper->toResource($entity); + } +} diff --git a/src/State/Accounting/AccountingStateProvider.php b/src/State/Accounting/AccountingStateProvider.php new file mode 100644 index 0000000..3982a52 --- /dev/null +++ b/src/State/Accounting/AccountingStateProvider.php @@ -0,0 +1,57 @@ +collectionProvider->provide($operation, $uriVariables, $context); + + $resources = []; + foreach ($accountings as $accounting) { + $resources[] = $this->toResource($accounting); + } + + return new TraversablePaginator( + new \ArrayIterator($resources), + $accountings->getCurrentPage(), + $accountings->getItemsPerPage(), + $accountings->getTotalItems() + ); + } + + $accounting = $this->itemProvider->provide($operation, $uriVariables, $context); + + return $this->toResource($accounting); + } + + private function toResource(?Entity\Accounting $accounting): ?ApiResource\Accounting + { + if ($accounting === null) { + return null; + } + + return $this->accountingMapper->toResource($accounting); + } +} 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); + } +} diff --git a/src/State/Gateway/CheckoutStateProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php new file mode 100644 index 0000000..f8d7509 --- /dev/null +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -0,0 +1,37 @@ +checkoutMapper->toEntity($data); + $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + + $entity = $this->gatewayLocator->getGateway($data->gateway->name)->process($entity); + $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + + return $this->checkoutMapper->toResource($entity); + } +} diff --git a/src/State/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php similarity index 77% rename from src/State/GatewayStateProvider.php rename to src/State/Gateway/GatewayStateProvider.php index 0ae771b..803a7e9 100644 --- a/src/State/GatewayStateProvider.php +++ b/src/State/Gateway/GatewayStateProvider.php @@ -1,41 +1,21 @@ 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'); - } - } + private GatewayMapper $gatewayMapper, + ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { @@ -46,4 +26,25 @@ class GatewayStateProvider implements ProviderInterface return $this->getGateway($uriVariables['name']); } } + + private function getGateways(): array + { + $gateways = []; + foreach ($this->gateways->getGateways() as $gateway) { + $gateways[] = $this->gatewayMapper->toResource($gateway); + } + + return $gateways; + } + + private function getGateway(string $name): Gateway + { + try { + $gateway = $this->gateways->getGateway($name); + + return $this->gatewayMapper->toResource($gateway); + } catch (\Exception $e) { + throw new NotFoundHttpException('Not Found'); + } + } } diff --git a/src/State/GatewayCheckoutProcessor.php b/src/State/GatewayCheckoutProcessor.php deleted file mode 100644 index 5935fe9..0000000 --- a/src/State/GatewayCheckoutProcessor.php +++ /dev/null @@ -1,34 +0,0 @@ -innerProcessor->process($data, $operation, $uriVariables, $context); - - $gateway = $this->gatewayLocator->getGatewayOf($checkout); - $checkout = $gateway->process($checkout); - - return $this->innerProcessor->process($data, $operation, $uriVariables, $context); - } -} 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/GatewayName.php b/src/Validator/GatewayName.php deleted file mode 100644 index 3772634..0000000 --- a/src/Validator/GatewayName.php +++ /dev/null @@ -1,21 +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(); - } - } -} 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(); + } + } + } +} diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/Accounting/AccountingTest.php similarity index 81% rename from tests/Entity/AccountingTest.php rename to tests/Entity/Accounting/AccountingTest.php index df69e3b..6d7f8e3 100644 --- a/tests/Entity/AccountingTest.php +++ b/tests/Entity/Accounting/AccountingTest.php @@ -1,8 +1,8 @@ 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(Tipjar::class, $accountingA->getOwnerClass()); - $this->assertEquals('TEST_TIPJAR_A', $accountingA->getTipjar()->getName()); + $this->assertEquals($tipjarA->getId(), $accountingA->getOwner()->getId()); + $this->assertEquals(Tipjar::class, $accountingA->getOwner()::class); $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(Tipjar::class, $accountingB->getOwnerClass()); - $this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName()); + $this->assertEquals($tipjarB->getId(), $accountingB->getOwner()->getId()); + $this->assertEquals(Tipjar::class, $accountingB->getOwner()::class); } } 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/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php new file mode 100644 index 0000000..d1cb253 --- /dev/null +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -0,0 +1,265 @@ +entityManager = static::getContainer()->get(EntityManagerInterface::class); + $this->walletService = static::getContainer()->get(WalletService::class); + } + + private function getUser(): User + { + $user = new User(); + $user->setUsername('wallettestuser'); + $user->setEmail('testuser@wallet.com'); + $user->setPassword('wallettestpassword'); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $user; + } + + private function getTipjar(): Tipjar + { + $tipjar = new Tipjar(); + $tipjar->setName('WALLET_TEST_TIPJAR'); + + $this->entityManager->persist($tipjar); + $this->entityManager->flush(); + + return $tipjar; + } + + public function testTransactionsAddFunds() + { + $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); + + /* + * $this->walletService->save($incoming); + * called automatically on Transaction persist. + * + * @see \App\EventListener\WalletTransactionsListener + */ + $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(StatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + } + + public function testTransactionsGetFinanced() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $outgoing = new Transaction(); + $outgoing->setMoney(new Money(20, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(80, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(2, $statements); + + $statement = $statements[0]; + $this->assertEquals(80, $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[1]; + $this->assertEquals(20, $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()); + + $outgoing = new Transaction(); + $outgoing->setMoney(new Money(30, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(50, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(3, $statements); + + $statement = $statements[0]; + $this->assertEquals(50, $statement->getBalance()->amount); + $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(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()[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() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(11, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $outgoing = new Transaction(); + $outgoing->setMoney(new Money(12, 'EUR')); + $outgoing->setOrigin($user); + $outgoing->setTarget($tipjar); + + $this->walletService->spend($outgoing); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(13, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $this->entityManager->persist($incoming); + $this->entityManager->flush(); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(22, $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(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(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(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(13, $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()); + } +} diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php new file mode 100644 index 0000000..949f4ca --- /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)); + } +}