diff --git a/.env b/.env index 8e0a5b7..ff59b19 100644 --- a/.env +++ b/.env @@ -34,5 +34,20 @@ DATABASE_URL="mysql://goteo:goteo@mariadb:3306/goteo?serverVersion=10.11.2-Maria CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### +###> doctrineencryptbundle/doctrine-encrypt-bundle ### +DOCTRINE_ENCRYPT_KEY="" +CRYPTO_SECRET="" +###< doctrineencryptbundle/doctrine-encrypt-bundle ### + +###> App\Gateway\Stripe\StripeGateway ### STRIPE_API_KEY="" STRIPE_WEBHOOK_SECRET="" +###< App\Gateway\Stripe\StripeGateway ### + +###> 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\Gateway\Paypal\PaypalGateway ### diff --git a/.env.test b/.env.test index 9e7162f..92426e3 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st' SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots +CRYPTO_SECRET="CryptoSecret" diff --git a/.github/workflows/php-cs-fixer.yaml b/.github/workflows/php-cs-fixer.yaml index 8b4ced3..1c3dea8 100644 --- a/.github/workflows/php-cs-fixer.yaml +++ b/.github/workflows/php-cs-fixer.yaml @@ -8,7 +8,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.2' - tools: php-cs-fixer + tools: php-cs-fixer:3.64 - name: Checkout repository uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 268ee02..5e361fb 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,13 @@ /.php-cs-fixer.php /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### + +###> XDebug ### +/cachegrind* +###< XDebug ### + +###> doctrineencryptbundle/doctrine-encrypt-bundle +.Halite.key +###< doctrineencryptbundle/doctrine-encrypt-bundle + +.php-version diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b8c750c..a7f6eb1 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,6 +8,9 @@ return (new PhpCsFixer\Config()) ->setRules([ '@Symfony' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'function_declaration' => ['closure_fn_spacing' => 'none'], + 'single_line_throw' => false, + 'single_line_empty_body' => true, 'yoda_style' => [ 'equal' => false, 'identical' => false, diff --git a/composer.json b/composer.json index 6956b4e..d0b66f6 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^2.19", + "doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4", "nelmio/cors-bundle": "^2.3", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", @@ -28,6 +29,7 @@ "symfony/expression-language": "6.4.*", "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", + "symfony/http-client": "6.4.*", "symfony/intl": "6.4.*", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", @@ -88,11 +90,10 @@ } }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.58", + "friendsofphp/php-cs-fixer": "3.64", "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", - "symfony/http-client": "6.4.*", "symfony/maker-bundle": "^1.51", "symfony/phpunit-bridge": "^7.0", "zenstruck/foundry": "^1.36" diff --git a/composer.lock b/composer.lock index d597c98..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": "f466394d7b46dccfdd81e934c576acfa", + "content-hash": "0d0e2940f9aaecab4ba3c50bc5822de6", "packages": [ { "name": "api-platform/core", - "version": "v3.3.11", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4" + "reference": "77d3ff3e8ddf88546eaf288a399d11f130553606" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/ea7d443376dfa1f9a05b53060049e7837d2ec7d4", - "reference": "ea7d443376dfa1f9a05b53060049e7837d2ec7d4", + "url": "https://api.github.com/repos/api-platform/core/zipball/77d3ff3e8ddf88546eaf288a399d11f130553606", + "reference": "77d3ff3e8ddf88546eaf288a399d11f130553606", "shasum": "" }, "require": { @@ -26,13 +26,13 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.4 || ^7.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.0", + "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.0" }, "conflict": { @@ -44,15 +44,56 @@ "elasticsearch/elasticsearch": ">=8.0,<8.4", "phpspec/prophecy": "<1.15", "phpunit/phpunit": "<9.5", - "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/framework-bundle": "6.4.6 || 7.1.6", "symfony/var-exporter": "<6.1.1" }, + "replace": { + "api-platform/doctrine-common": "self.version", + "api-platform/doctrine-odm": "self.version", + "api-platform/doctrine-orm": "self.version", + "api-platform/documentation": "self.version", + "api-platform/elasticsearch": "self.version", + "api-platform/graphql": "self.version", + "api-platform/http-cache": "self.version", + "api-platform/hydra": "self.version", + "api-platform/json-api": "self.version", + "api-platform/json-hal": "self.version", + "api-platform/json-schema": "self.version", + "api-platform/jsonld": "self.version", + "api-platform/laravel": "self.version", + "api-platform/metadata": "self.version", + "api-platform/openapi": "self.version", + "api-platform/parameter-validator": "self.version", + "api-platform/ramsey-uuid": "self.version", + "api-platform/serializer": "self.version", + "api-platform/state": "self.version", + "api-platform/symfony": "self.version", + "api-platform/validator": "self.version" + }, "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", + "api-platform/http-cache": "^3.4 || ^4.0", + "api-platform/hydra": "^3.4 || ^4.0", + "api-platform/json-api": "^3.3 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/jsonld": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/parameter-validator": "^3.4", + "api-platform/ramsey-uuid": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "api-platform/validator": "^3.4 || ^4.0", "behat/behat": "^3.11", "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", - "doctrine/dbal": "^3.4.0", + "doctrine/dbal": "^3.4.0 || ^4.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", "doctrine/mongodb-odm": "^2.2", "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", @@ -61,7 +102,7 @@ "friends-of-behat/mink-browserkit-driver": "^1.3.1", "friends-of-behat/mink-extension": "^2.2", "friends-of-behat/symfony-extension": "^2.1", - "guzzlehttp/guzzle": "^6.0 || ^7.0", + "guzzlehttp/guzzle": "^6.0 || ^7.1", "jangregor/phpstan-prophecy": "^1.0", "justinrainbow/json-schema": "^5.2.1", "phpspec/prophecy-phpunit": "^2.0", @@ -74,41 +115,42 @@ "phpunit/phpunit": "^9.6", "psr/log": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^3.9.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4 || ^2.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", "sebastian/comparator": "<5.0", "soyuka/contexts": "v3.3.9", - "soyuka/pmu": "^0.0.2", + "soyuka/pmu": "^0.0.12", "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.4 || ^7.0", - "symfony/browser-kit": "^6.4 || ^7.0", - "symfony/cache": "^6.4 || ^7.0", - "symfony/config": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/css-selector": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0.12", - "symfony/doctrine-bridge": "^6.4 || ^7.0", - "symfony/dom-crawler": "^6.4 || ^7.0", - "symfony/error-handler": "^6.4 || ^7.0", - "symfony/event-dispatcher": "^6.4 || ^7.0", - "symfony/expression-language": "^6.4 || ^7.0", - "symfony/finder": "^6.4 || ^7.0", - "symfony/form": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/intl": "^6.4 || ^7.0", + "symfony/asset": "^6.4 || ^7.1", + "symfony/browser-kit": "^6.4 || ^7.1", + "symfony/cache": "^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.1", + "symfony/console": "^6.4 || ^7.1", + "symfony/css-selector": "^6.4 || ^7.1", + "symfony/dependency-injection": "^6.4 || ^7.1", + "symfony/doctrine-bridge": "^6.4 || ^7.1", + "symfony/dom-crawler": "^6.4 || ^7.1", + "symfony/error-handler": "^6.4 || ^7.1", + "symfony/event-dispatcher": "^6.4 || ^7.1", + "symfony/expression-language": "^6.4 || ^7.1", + "symfony/finder": "^6.4 || ^7.1", + "symfony/form": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", + "symfony/http-client": "^6.4 || ^7.1", + "symfony/intl": "^6.4 || ^7.1", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4.1 || ^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/security-bundle": "^6.4 || ^7.0", - "symfony/security-core": "^6.4 || ^7.0", - "symfony/stopwatch": "^6.4 || ^7.0", - "symfony/twig-bundle": "^6.4 || ^7.0", - "symfony/uid": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "symfony/web-profiler-bundle": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.1", + "symfony/phpunit-bridge": "^6.4.1 || ^7.1", + "symfony/routing": "^6.4 || ^7.1", + "symfony/security-bundle": "^6.4 || ^7.1", + "symfony/security-core": "^6.4 || ^7.1", + "symfony/stopwatch": "^6.4 || ^7.1", + "symfony/string": "^6.4 || ^7.1", + "symfony/twig-bundle": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.1", + "symfony/yaml": "^6.4 || ^7.1", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "webonyx/graphql-php": "^14.0 || ^15.0" }, @@ -133,31 +175,22 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.1" }, - "projects": [ - "api-platform/doctrine-common", - "api-platform/doctrine-orm", - "api-platform/doctrine-odm", - "api-platform/metadata", - "api-platform/json-schema", - "api-platform/elasticsearch", - "api-platform/jsonld", - "api-platform/hydra", - "api-platform/openapi", - "api-platform/graphql", - "api-platform/http-cache", - "api-platform/documentation", - "api-platform/parameter-validator", - "api-platform/ramsey-uuid", - "api-platform/serializer", - "api-platform/state", - "api-platform/symfony", - "api-platform/validator" - ] + "pmu": { + "projects": [ + "./src/*/composer.json", + "src/Doctrine/*/composer.json" + ] + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } }, "autoload": { "psr-4": { @@ -190,9 +223,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.3.11" + "source": "https://github.com/api-platform/core/tree/v3.4.4" }, - "time": "2024-07-20T08:19:33+00:00" + "time": "2024-10-19T10:05:07+00:00" }, { "name": "beberlei/doctrineextensions", @@ -418,6 +451,82 @@ ], "time": "2023-09-23T21:17:11+00:00" }, + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "time": "2024-09-05T10:17:24+00:00" + }, { "name": "doctrine/cache", "version": "2.2.0", @@ -599,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": { @@ -670,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": [ { @@ -686,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.8.6", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -715,12 +824,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.11.5", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "9.6.19", + "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.1", + "squizlabs/php_codesniffer": "3.10.2", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -783,7 +892,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.6" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -799,7 +908,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T10:38:17+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -850,16 +959,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "5418e811a14724068e95e0ba43353b903ada530f" + "reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f", - "reference": "5418e811a14724068e95e0ba43353b903ada530f", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca59d84b8e63143ce1aed90cdb333ba329d71563", + "reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563", "shasum": "" }, "require": { @@ -950,7 +1059,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.0" }, "funding": [ { @@ -966,7 +1075,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T07:20:37+00:00" + "time": "2024-09-01T09:46:40+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1391,16 +1500,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.0", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "535a70dcbd88b8c6ba945be050977457f4f4c06c" + "reference": "5007eb1168691225ac305fe16856755c20860842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/535a70dcbd88b8c6ba945be050977457f4f4c06c", - "reference": "535a70dcbd88b8c6ba945be050977457f4f4c06c", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", + "reference": "5007eb1168691225ac305fe16856755c20860842", "shasum": "" }, "require": { @@ -1423,6 +1532,7 @@ "doctrine/persistence": "^2 || ^3", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", + "fig/log-test": "^1", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.3", @@ -1473,7 +1583,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.0" + "source": "https://github.com/doctrine/migrations/tree/3.8.2" }, "funding": [ { @@ -1489,20 +1599,20 @@ "type": "tidelift" } ], - "time": "2024-06-26T14:12:46+00:00" + "time": "2024-10-10T21:35:27+00:00" }, { "name": "doctrine/orm", - "version": "2.19.6", + "version": "2.20.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073" + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073", - "reference": "c1bb2ccf4b19c845f91ff7c4c01dc7cbba7f4073", + "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c", + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c", "shasum": "" }, "require": { @@ -1531,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", @@ -1588,9 +1700,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.19.6" + "source": "https://github.com/doctrine/orm/tree/2.20.0" }, - "time": "2024-06-26T17:24:40+00:00" + "time": "2024-10-11T11:47:24+00:00" }, { "name": "doctrine/persistence", @@ -1691,16 +1803,16 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" + "reference": "b784cbde727cf806721451dde40eff4fec3bbe86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b784cbde727cf806721451dde40eff4fec3bbe86", + "reference": "b784cbde727cf806721451dde40eff4fec3bbe86", "shasum": "" }, "require": { @@ -1708,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" @@ -1740,22 +1853,87 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.1" }, - "time": "2024-05-08T08:12:09+00:00" + "time": "2024-10-21T18:21:57+00:00" }, { - "name": "gedmo/doctrine-extensions", - "version": "v3.16.1", + "name": "doctrineencryptbundle/doctrine-encrypt-bundle", + "version": "5.4.1", "source": { "type": "git", - "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d" + "url": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle.git", + "reference": "556c2ec0156d519378e6d62a61c2e924e4583e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/e85560ed96f977b8c29428a99222cb2ef2f0e80d", - "reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d", + "url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/556c2ec0156d519378e6d62a61c2e924e4583e13", + "reference": "556c2ec0156d519378e6d62a61c2e924e4583e13", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.13|^2.0", + "doctrine/doctrine-bundle": "^2.0.8|^2.1", + "doctrine/orm": "^2.5", + "paragonie/halite": "^4.6|^5.0", + "php": "^7.2|^8.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "defuse/php-encryption": "^2.1", + "doctrine/cache": "^1.11", + "jetbrains/phpstorm-attributes": "^1.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^7.0" + }, + "suggest": { + "defuse/php-encryption": "Alternative for halite for use with older php-versions", + "ext-sodium": "Required to use halite encryption library.", + "paragonie/sodium_compat": "Alternative for ext-sodium for use with older php-versions" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ambta\\DoctrineEncryptBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Encrypted symfony entity's by verified and standardized libraries", + "keywords": [ + "Defuse", + "decrypt", + "doctrine", + "encrypt", + "halite", + "symfony" + ], + "support": { + "issues": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/issues", + "source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.1" + }, + "time": "2024-10-13T10:08:12+00:00" + }, + { + "name": "gedmo/doctrine-extensions", + "version": "v3.17.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", + "reference": "eabb45018c5a4362b46c5beae3881261da89f900" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900", + "reference": "eabb45018c5a4362b46c5beae3881261da89f900", "shasum": "" }, "require": { @@ -1772,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", @@ -1851,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": [ @@ -1872,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", @@ -2104,6 +2282,287 @@ }, "time": "2024-06-24T21:25:28+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/halite", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/paragonie/halite.git", + "reference": "aee234711b29cccb4a17aaaf6104fc542862fc1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/halite/zipball/aee234711b29cccb4a17aaaf6104fc542862fc1e", + "reference": "aee234711b29cccb4a17aaaf6104fc542862fc1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "paragonie/constant_time_encoding": "^2|^3", + "paragonie/hidden-string": "^1|^2", + "paragonie/sodium_compat": "^1|^2", + "php": "^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\Halite\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "High-level cryptography interface powered by libsodium", + "homepage": "https://github.com/paragonie/halite", + "keywords": [ + "Argon2i", + "BLAKE", + "BLAKE2", + "BLAKE2b", + "Curve25519", + "Ed25519", + "X25519", + "Xsalsa20", + "argon2", + "cryptography", + "encryption", + "ext-sodium", + "hashing", + "libsodium", + "password", + "public-key", + "signatures", + "sodium" + ], + "support": { + "docs": "https://github.com/paragonie/halite/tree/master/doc", + "issues": "https://github.com/paragonie/halite/issues", + "source": "https://github.com/paragonie/halite/tree/v5.1.2" + }, + "time": "2024-05-08T12:59:43+00:00" + }, + { + "name": "paragonie/hidden-string", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/hidden-string.git", + "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7", + "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2|^3", + "php": "^7.4|^8" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\HiddenString\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Encapsulate strings in an object to hide them from stack traces", + "homepage": "https://github.com/paragonie/hidden-string", + "keywords": [ + "hidden", + "stack trace", + "string" + ], + "support": { + "issues": "https://github.com/paragonie/hidden-string/issues", + "source": "https://github.com/paragonie/hidden-string/tree/v2.2.0" + }, + "time": "2024-05-08T12:45:06+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" + }, + "time": "2024-09-04T12:51:01+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2281,16 +2740,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", "shasum": "" }, "require": { @@ -2322,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.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "psr/cache", @@ -2584,16 +3043,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -2628,9 +3087,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "stof/doctrine-extensions-bundle", @@ -2714,16 +3173,16 @@ }, { "name": "stripe/stripe-php", - "version": "v15.4.0", + "version": "v15.10.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "13581553ea8725b9d0cfff93d24462c4149abec9" + "reference": "3df1a19a33477af9ead8984dbd84e8f637c36199" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/13581553ea8725b9d0cfff93d24462c4149abec9", - "reference": "13581553ea8725b9d0cfff93d24462c4149abec9", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/3df1a19a33477af9ead8984dbd84e8f637c36199", + "reference": "3df1a19a33477af9ead8984dbd84e8f637c36199", "shasum": "" }, "require": { @@ -2767,9 +3226,9 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v15.4.0" + "source": "https://github.com/stripe/stripe-php/tree/v15.10.0" }, - "time": "2024-07-25T21:00:20+00:00" + "time": "2024-09-18T18:38:30+00:00" }, { "name": "subiabre/doctrine-snowflakes", @@ -2886,16 +3345,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc" + "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/6702d2d777260e6ff3451fee2d7d78ab5f715cdc", - "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc", + "url": "https://api.github.com/repos/symfony/cache/zipball/a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", + "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8", "shasum": "" }, "require": { @@ -2962,7 +3421,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.10" + "source": "https://github.com/symfony/cache/tree/v6.4.12" }, "funding": [ { @@ -2978,7 +3437,7 @@ "type": "tidelift" } ], - "time": "2024-07-17T06:05:49+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/cache-contracts", @@ -3207,16 +3666,16 @@ }, { "name": "symfony/console", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", + "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { @@ -3281,7 +3740,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.10" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -3297,20 +3756,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb" + "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", - "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e", + "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e", "shasum": "" }, "require": { @@ -3362,7 +3821,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.12" }, "funding": [ { @@ -3378,7 +3837,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T07:32:07+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3449,16 +3908,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "0de9662441bce4670506d0c371cc819a9d0a7607" + "reference": "b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/0de9662441bce4670506d0c371cc819a9d0a7607", - "reference": "0de9662441bce4670506d0c371cc819a9d0a7607", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af", + "reference": "b5c0a0172bbe9b1435181d94ca5cbe4af3fb45af", "shasum": "" }, "require": { @@ -3537,7 +3996,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.10" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.12" }, "funding": [ { @@ -3553,20 +4012,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-08T12:31:10+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e" + "reference": "815284236cab7d8e1280f53bf562c07a4dfe5954" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/2ae0c84cc9be0dc1eeb86016970b63c764d8472e", - "reference": "2ae0c84cc9be0dc1eeb86016970b63c764d8472e", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/815284236cab7d8e1280f53bf562c07a4dfe5954", + "reference": "815284236cab7d8e1280f53bf562c07a4dfe5954", "shasum": "" }, "require": { @@ -3611,7 +4070,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.10" + "source": "https://github.com/symfony/dotenv/tree/v6.4.12" }, "funding": [ { @@ -3627,7 +4086,7 @@ "type": "tidelift" } ], - "time": "2024-07-09T18:29:35+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/error-handler", @@ -3862,16 +4321,16 @@ }, { "name": "symfony/expression-language", - "version": "v6.4.8", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a" + "reference": "564e109c40d3637053c942a29a58e9434592a8bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", - "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/564e109c40d3637053c942a29a58e9434592a8bf", + "reference": "564e109c40d3637053c942a29a58e9434592a8bf", "shasum": "" }, "require": { @@ -3906,7 +4365,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.8" + "source": "https://github.com/symfony/expression-language/tree/v6.4.11" }, "funding": [ { @@ -3922,20 +4381,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-08-12T09:55:28+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/f810e3cbdf7fdc35983968523d09f349fa9ada12", + "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12", "shasum": "" }, "require": { @@ -3972,7 +4431,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + "source": "https://github.com/symfony/filesystem/tree/v6.4.12" }, "funding": [ { @@ -3988,20 +4447,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-09-16T16:01:33+00:00" }, { "name": "symfony/finder", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "af29198d87112bebdd397bd7735fbd115997824c" + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", - "reference": "af29198d87112bebdd397bd7735fbd115997824c", + "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453", "shasum": "" }, "require": { @@ -4036,7 +4495,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.10" + "source": "https://github.com/symfony/finder/tree/v6.4.11" }, "funding": [ { @@ -4052,26 +4511,29 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:06:38+00:00" + "time": "2024-08-13T14:27:37+00:00" }, { "name": "symfony/flex", - "version": "v2.4.5", + "version": "v2.4.7", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "b0a405f40614c9f584b489d54f91091817b0e26e" + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/b0a405f40614c9f584b489d54f91091817b0e26e", - "reference": "b0a405f40614c9f584b489d54f91091817b0e26e", + "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", @@ -4101,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.5" + "source": "https://github.com/symfony/flex/tree/v2.4.7" }, "funding": [ { @@ -4117,20 +4579,20 @@ "type": "tidelift" } ], - "time": "2024-03-02T08:16:47+00:00" + "time": "2024-10-07T08:51:54+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b" + "reference": "6a9665bd1fae37b198429775c6132f193339434f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6cbdb0cc3ddbb63499262cd3036882b08ee2690b", - "reference": "6cbdb0cc3ddbb63499262cd3036882b08ee2690b", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a9665bd1fae37b198429775c6132f193339434f", + "reference": "6a9665bd1fae37b198429775c6132f193339434f", "shasum": "" }, "require": { @@ -4139,7 +4601,7 @@ "php": ">=8.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.12|^7.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.1|^7.0", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", @@ -4249,7 +4711,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.10" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.12" }, "funding": [ { @@ -4265,20 +4727,191 @@ "type": "tidelift" } ], - "time": "2024-07-26T13:24:20+00:00" + "time": "2024-09-20T13:34:56+00:00" }, { - "name": "symfony/http-foundation", - "version": "v6.4.10", + "name": "symfony/http-client", + "version": "v6.4.12", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b" + "url": "https://github.com/symfony/http-client.git", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/117f1f20a7ade7bcea28b861fb79160a21a1e37b", - "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:21:33+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "133ac043875f59c26c55e79cf074562127cce4d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/133ac043875f59c26c55e79cf074562127cce4d2", + "reference": "133ac043875f59c26c55e79cf074562127cce4d2", "shasum": "" }, "require": { @@ -4326,7 +4959,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.10" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.12" }, "funding": [ { @@ -4342,20 +4975,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:36:27+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "147e0daf618d7575b5007055340d09aece5cf068" + "reference": "96df83d51b5f78804f70c093b97310794fd6257b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/147e0daf618d7575b5007055340d09aece5cf068", - "reference": "147e0daf618d7575b5007055340d09aece5cf068", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96df83d51b5f78804f70c093b97310794fd6257b", + "reference": "96df83d51b5f78804f70c093b97310794fd6257b", "shasum": "" }, "require": { @@ -4440,7 +5073,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.10" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.12" }, "funding": [ { @@ -4456,20 +5089,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T14:52:04+00:00" + "time": "2024-09-21T06:02:57+00:00" }, { "name": "symfony/intl", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb" + "reference": "5f64d7218f4078492ca59da94747d7474a2a52c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/50265cdcf5a44bec3fcf487b5d0015aece91d1eb", - "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb", + "url": "https://api.github.com/repos/symfony/intl/zipball/5f64d7218f4078492ca59da94747d7474a2a52c4", + "reference": "5f64d7218f4078492ca59da94747d7474a2a52c4", "shasum": "" }, "require": { @@ -4523,7 +5156,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.8" + "source": "https://github.com/symfony/intl/tree/v6.4.12" }, "funding": [ { @@ -4539,7 +5172,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-20T08:16:53+00:00" }, { "name": "symfony/monolog-bridge", @@ -4775,20 +5408,20 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -4833,7 +5466,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -4849,24 +5482,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -4914,7 +5547,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -4930,24 +5563,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -4994,7 +5627,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -5010,24 +5643,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -5070,7 +5703,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -5086,20 +5719,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:35:24+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/property-access", - "version": "v6.4.8", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a" + "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/e4d9b00983612f9c0013ca37c61affdba2dd975a", - "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a", + "url": "https://api.github.com/repos/symfony/property-access/zipball/866f6cd84f2094cbc6f66ce9752faf749916e2a9", + "reference": "866f6cd84f2094cbc6f66ce9752faf749916e2a9", "shasum": "" }, "require": { @@ -5147,7 +5780,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.8" + "source": "https://github.com/symfony/property-access/tree/v6.4.11" }, "funding": [ { @@ -5163,7 +5796,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-08-30T16:10:11+00:00" }, { "name": "symfony/property-info", @@ -5250,16 +5883,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87" + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/aad19fe10753ba842f0d653a8db819c4b3affa87", - "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87", + "url": "https://api.github.com/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", "shasum": "" }, "require": { @@ -5313,7 +5946,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.10" + "source": "https://github.com/symfony/routing/tree/v6.4.12" }, "funding": [ { @@ -5329,20 +5962,20 @@ "type": "tidelift" } ], - "time": "2024-07-15T09:26:24+00:00" + "time": "2024-09-20T08:32:26+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0" + "reference": "bfe32a1adf41da4dd7f6b939a039779d7af5497f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", - "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", + "url": "https://api.github.com/repos/symfony/runtime/zipball/bfe32a1adf41da4dd7f6b939a039779d7af5497f", + "reference": "bfe32a1adf41da4dd7f6b939a039779d7af5497f", "shasum": "" }, "require": { @@ -5392,7 +6025,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.8" + "source": "https://github.com/symfony/runtime/tree/v6.4.12" }, "funding": [ { @@ -5408,20 +6041,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-19T13:29:10+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "50007f4f76632741b62fa9604c5f65807f268b72" + "reference": "620be16fceded671823ce6332d06f44bb327096d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/50007f4f76632741b62fa9604c5f65807f268b72", - "reference": "50007f4f76632741b62fa9604c5f65807f268b72", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/620be16fceded671823ce6332d06f44bb327096d", + "reference": "620be16fceded671823ce6332d06f44bb327096d", "shasum": "" }, "require": { @@ -5430,7 +6063,7 @@ "php": ">=8.1", "symfony/clock": "^6.3|^7.0", "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.2|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "symfony/http-foundation": "^6.2|^7.0", @@ -5504,7 +6137,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.10" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.11" }, "funding": [ { @@ -5520,20 +6153,20 @@ "type": "tidelift" } ], - "time": "2024-07-17T10:49:44+00:00" + "time": "2024-08-20T11:22:16+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "432dec55da108c471adcf58c351af01372a52164" + "reference": "8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/432dec55da108c471adcf58c351af01372a52164", - "reference": "432dec55da108c471adcf58c351af01372a52164", + "url": "https://api.github.com/repos/symfony/security-core/zipball/8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f", + "reference": "8c7e52155262b3ef6b7885f6d9bd90fb24eaa66f", "shasum": "" }, "require": { @@ -5590,7 +6223,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.10" + "source": "https://github.com/symfony/security-core/tree/v6.4.12" }, "funding": [ { @@ -5606,7 +6239,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/security-csrf", @@ -5678,16 +6311,16 @@ }, { "name": "symfony/security-http", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "8e70f39626ada36c5492c3aff9369c85d2840948" + "reference": "f6df97af71943cda726dc852335204eac02a716b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/8e70f39626ada36c5492c3aff9369c85d2840948", - "reference": "8e70f39626ada36c5492c3aff9369c85d2840948", + "url": "https://api.github.com/repos/symfony/security-http/zipball/f6df97af71943cda726dc852335204eac02a716b", + "reference": "f6df97af71943cda726dc852335204eac02a716b", "shasum": "" }, "require": { @@ -5746,7 +6379,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.9" + "source": "https://github.com/symfony/security-http/tree/v6.4.12" }, "funding": [ { @@ -5762,20 +6395,20 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718" + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/9a67fcf320561e96f94d62bbe0e169ac534a5718", - "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718", + "url": "https://api.github.com/repos/symfony/serializer/zipball/10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", "shasum": "" }, "require": { @@ -5844,7 +6477,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.10" + "source": "https://github.com/symfony/serializer/tree/v6.4.12" }, "funding": [ { @@ -5860,7 +6493,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T13:13:26+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/service-contracts", @@ -6009,16 +6642,16 @@ }, { "name": "symfony/string", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ccf9b30251719567bfd46494138327522b9a9446" + "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", - "reference": "ccf9b30251719567bfd46494138327522b9a9446", + "url": "https://api.github.com/repos/symfony/string/zipball/f8a1ccebd0997e16112dfecfd74220b78e5b284b", + "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b", "shasum": "" }, "require": { @@ -6075,7 +6708,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.10" + "source": "https://github.com/symfony/string/tree/v6.4.12" }, "funding": [ { @@ -6091,7 +6724,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:21:14+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/translation-contracts", @@ -6173,16 +6806,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v6.4.9", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "9bcb26445b9d4ef1087c389234bf33fb00e10ea6" + "reference": "09c0df13f822a1b80c5972ca1aa9eeb1288e1194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/9bcb26445b9d4ef1087c389234bf33fb00e10ea6", - "reference": "9bcb26445b9d4ef1087c389234bf33fb00e10ea6", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/09c0df13f822a1b80c5972ca1aa9eeb1288e1194", + "reference": "09c0df13f822a1b80c5972ca1aa9eeb1288e1194", "shasum": "" }, "require": { @@ -6262,7 +6895,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.9" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.12" }, "funding": [ { @@ -6278,20 +6911,20 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-09-15T06:35:36+00:00" }, { "name": "symfony/twig-bundle", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65" + "reference": "4e63369647e3924e110b37337c6a58aac3086ad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", - "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/4e63369647e3924e110b37337c6a58aac3086ad4", + "reference": "4e63369647e3924e110b37337c6a58aac3086ad4", "shasum": "" }, "require": { @@ -6346,7 +6979,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v6.4.8" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.12" }, "funding": [ { @@ -6362,20 +6995,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-08T12:30:05+00:00" }, { "name": "symfony/validator", - "version": "v6.4.10", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd" + "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", - "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", + "url": "https://api.github.com/repos/symfony/validator/zipball/6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0", + "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0", "shasum": "" }, "require": { @@ -6443,7 +7076,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.10" + "source": "https://github.com/symfony/validator/tree/v6.4.12" }, "funding": [ { @@ -6459,20 +7092,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-09-20T08:18:25+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.10", + "version": "v6.4.11", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" + "reference": "ee14c8254a480913268b1e3b1cba8045ed122694" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ee14c8254a480913268b1e3b1cba8045ed122694", + "reference": "ee14c8254a480913268b1e3b1cba8045ed122694", "shasum": "" }, "require": { @@ -6528,7 +7161,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.11" }, "funding": [ { @@ -6544,7 +7177,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2024-08-30T16:03:21+00:00" }, { "name": "symfony/var-exporter", @@ -6708,16 +7341,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", - "reference": "52903de178d542850f6f341ba92995d3d63e60c9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -6760,7 +7393,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.8" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -6776,28 +7409,28 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "twig/twig", - "version": "v3.10.3", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22" + "symfony/polyfill-php81": "^1.29" }, "require-dev": { "psr/container": "^1.0|^2.0", @@ -6843,7 +7476,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.10.3" + "source": "https://github.com/twigphp/Twig/tree/v3.14.0" }, "funding": [ { @@ -6855,7 +7488,7 @@ "type": "tidelift" } ], - "time": "2024-05-16T10:04:27+00:00" + "time": "2024-09-09T17:55:12+00:00" }, { "name": "webmozart/assert", @@ -7039,26 +7672,26 @@ }, { "name": "composer/pcre", - "version": "3.2.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.8" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan": "^1.11.10", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^8 || ^9" }, @@ -7098,7 +7731,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -7114,28 +7747,28 @@ "type": "tidelift" } ], - "time": "2024-07-25T09:36:02+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -7179,7 +7812,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -7195,7 +7828,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -7375,16 +8008,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -7424,7 +8057,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -7432,20 +8065,20 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.61.1", + "version": "v3.64.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" + "reference": "58dd9c931c785a79739310aef5178928305ffa67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", "shasum": "" }, "require": { @@ -7527,7 +8160,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" }, "funding": [ { @@ -7535,7 +8168,7 @@ "type": "github" } ], - "time": "2024-07-31T14:33:15+00:00" + "time": "2024-08-30T23:09:38+00:00" }, { "name": "masterminds/html5", @@ -7666,16 +8299,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -7718,9 +8351,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-07-01T20:03:41+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phar-io/manifest", @@ -7842,35 +8475,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -7879,7 +8512,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -7908,7 +8541,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -7916,7 +8549,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -8161,16 +8794,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { @@ -8185,7 +8818,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -8244,7 +8877,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -8260,7 +8893,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "react/cache", @@ -9890,16 +10523,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "105b56a0305d219349edeb60a800082eca864e4b" + "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/105b56a0305d219349edeb60a800082eca864e4b", - "reference": "105b56a0305d219349edeb60a800082eca864e4b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/9d307ecbcb917001692be333cdc58f474fdb37f0", + "reference": "9d307ecbcb917001692be333cdc58f474fdb37f0", "shasum": "" }, "require": { @@ -9937,7 +10570,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.8" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.12" }, "funding": [ { @@ -9953,191 +10586,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" - }, - { - "name": "symfony/http-client", - "version": "v6.4.10", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.10" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-07-15T09:26:24+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-15T06:35:36+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.60.0", + "version": "v1.61.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "c305a02a22974670f359d4274c9431e1a191f559" + "reference": "a3b7f14d349f8f44ed752d4dde2263f77510cc18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c305a02a22974670f359d4274c9431e1a191f559", - "reference": "c305a02a22974670f359d4274c9431e1a191f559", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/a3b7f14d349f8f44ed752d4dde2263f77510cc18", + "reference": "a3b7f14d349f8f44ed752d4dde2263f77510cc18", "shasum": "" }, "require": { @@ -10200,7 +10662,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.60.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.61.0" }, "funding": [ { @@ -10216,7 +10678,7 @@ "type": "tidelift" } ], - "time": "2024-06-10T06:03:18+00:00" + "time": "2024-08-29T22:50:23+00:00" }, { "name": "symfony/options-resolver", @@ -10287,16 +10749,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1" + "reference": "e876eb90e32a8fc4c4911d458e09f88d65877d1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e823122d31935eb711e2767c31f3d71cb0b87fb1", - "reference": "e823122d31935eb711e2767c31f3d71cb0b87fb1", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e876eb90e32a8fc4c4911d458e09f88d65877d1c", + "reference": "e876eb90e32a8fc4c4911d458e09f88d65877d1c", "shasum": "" }, "require": { @@ -10349,7 +10811,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.4" }, "funding": [ { @@ -10365,20 +10827,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/process", - "version": "v6.4.8", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "url": "https://api.github.com/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", "shasum": "" }, "require": { @@ -10410,7 +10872,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.8" + "source": "https://github.com/symfony/process/tree/v6.4.12" }, "funding": [ { @@ -10426,7 +10888,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "theseer/tokenizer", @@ -10690,7 +11152,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10698,6 +11160,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index 070b2d5..19b2cda 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -12,4 +12,5 @@ return [ Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine_encrypt_bundle.yaml b/config/packages/doctrine_encrypt_bundle.yaml new file mode 100644 index 0000000..1a17a89 --- /dev/null +++ b/config/packages/doctrine_encrypt_bundle.yaml @@ -0,0 +1,11 @@ +ambta_doctrine_encrypt: + # If you want, you can use your own Encryptor. Encryptor must implements EncryptorInterface interface + # Default: Halite + # encryptor_class: Halite + + # Path where to store the keyfiles + # Default: '%kernel.project_dir%' + # secret_directory_path: '%kernel.project_dir%' + + # Secret string + secret: '%env(CRYPTO_SECRET)%' diff --git a/config/services.yaml b/config/services.yaml index 416b148..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,14 +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)%' - # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/docker-compose.yml b/docker-compose.yml index ff9598b..8bfe002 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,6 @@ services: container_name: goteo-v4-php volumes: - .:/app - - ./docker/php/conf.d/apc.ini:/usr/local/etc/php/conf.d/apc.ini:ro - ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro nginx: diff --git a/docker/php/conf.d/apc.ini b/docker/php/conf.d/apc.ini deleted file mode 100644 index 01842b6..0000000 --- a/docker/php/conf.d/apc.ini +++ /dev/null @@ -1,7 +0,0 @@ -[apc] -apc.enabled=1 -apc.enable_cli=1 -apc.shm_size=32M -apc.ttl=0 -apc.gc_ttl=3600 -apc.mmap_file_mask=/tmp/apc.XXXXXX 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/docker/php/conf.d/xdebug.dev.ini b/docker/php/conf.d/xdebug.dev.ini index a658c4c..30f48bf 100644 --- a/docker/php/conf.d/xdebug.dev.ini +++ b/docker/php/conf.d/xdebug.dev.ini @@ -4,3 +4,4 @@ xdebug.client_host=host.docker.internal xdebug.client_port=9003 xdebug.discover_client_host=0 xdebug.start_with_request=trigger +xdebug.output_dir=/app diff --git a/src/ApiResource/Accounting/Accounting.php b/src/ApiResource/Accounting/Accounting.php new file mode 100644 index 0000000..6b446bc --- /dev/null +++ b/src/ApiResource/Accounting/Accounting.php @@ -0,0 +1,37 @@ +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 103e649..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; @@ -23,9 +23,8 @@ class Version { public function __construct( private readonly LogEntry $log, - private readonly object $entity - ) { - } + 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 6d5a2c0..5fe6179 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -3,12 +3,15 @@ namespace App\Command; use App\Library\Benzina\Benzina; +use App\Library\Benzina\Pump\PumpInterface; use App\Library\Benzina\Stream\PdoStream; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -19,7 +22,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class BenzinaPumpCommand extends Command { public function __construct( - private Benzina $benzina + private Benzina $benzina, ) { parent::__construct(); } @@ -28,6 +31,14 @@ class BenzinaPumpCommand extends Command { $this->addArgument('table', InputArgument::REQUIRED); + $this->addOption( + 'database', + null, + InputOption::VALUE_OPTIONAL, + 'The address of the database to read from', + 'mysql://goteo:goteo@mariadb:3306/benzina' + ); + $this->addOption( 'batch-size', null, @@ -37,18 +48,11 @@ class BenzinaPumpCommand extends Command ); $this->addOption( - 'no-progress', + 'skip-pumped', null, - InputOption::VALUE_NONE, - 'For progressive pumps, disables the override of already pumped progress' - ); - - $this->addOption( - 'database', - null, - InputOption::VALUE_OPTIONAL, - 'The address of the database to read from', - 'mysql://goteo:goteo@mariadb:3306/benzina' + InputOption::VALUE_NEGATABLE, + 'Skips feeding already pumped records in a batch', + true ); $this->addUsage('app:benzina:pump --no-debug user'); @@ -65,61 +69,99 @@ EOF ); } + /** + * @param ConsoleOutputInterface $output + */ protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new SymfonyStyle($input, $output); + $batchSize = $input->getOption('batch-size'); $stream = new PdoStream( $input->getOption('database'), $input->getArgument('table'), - $input->getOption('batch-size') + $batchSize ); $streamSize = $stream->size(); + $streamBatches = $streamSize / $batchSize; + $streamSection = new SymfonyStyle($input, $output->section()); if ($streamSize < 1) { - $io->writeln('No data found at the given source. Skipping execution.'); + $streamSection->writeln('No data found at the given source. Skipping execution.'); return Command::SUCCESS; } - $pumps = $this->benzina->getPumps($stream); - $pumpsCount = count($pumps); + $streamSection->writeln(sprintf('Streaming %d records (%d batches).', $streamSize, $streamBatches)); + + $pumps = $this->benzina->getPumpsFor($stream); + $pumpsCount = \count($pumps); + $pumpsSection = new SymfonyStyle($input, $output->section()); if ($pumpsCount < 1) { - $io->writeln('No pumps available for the data. Skipping execution.'); + $pumpsSection->writeln('No pumps support the streamed sample. Skipping execution.'); return Command::SUCCESS; } - $io->writeln(sprintf('Found %d pumps.', $pumpsCount)); - $io->listing(\array_map(function ($pump) { + $pumpsSection->writeln(sprintf('Streaming to %d pumps.', $pumpsCount)); + $pumpsSection->listing(\array_map(function (PumpInterface $pump) { return $pump::class; }, $pumps)); - $io->writeln(sprintf('Processing %d records.', $streamSize)); - $progress = $io->createProgressBar(); - $progress->start($streamSize); + $streamedBatchesSection = $output->section(); + $streamedBatchesSection->writeln("\tStreamed batches:"); + $streamedBatches = new ProgressBar($streamedBatchesSection); + $streamedBatches->start($streamBatches); + + $streamedRecordsSection = $output->section(); + $streamedRecordsSection->writeln("\tStreamed records:"); + $streamedRecords = new ProgressBar($streamedRecordsSection); + $streamedRecords->start($streamSize); + + $memUsageSection = $output->section(); + $memUsageSection->writeln("\tMemory usage:"); + $memUsage = new ProgressBar($memUsageSection); + $memUsage->start($this->getMemoryLimit()); while (!$stream->eof()) { - $data = $stream->read(); + \memory_reset_peak_usage(); - foreach ($pumps as $pump) { - $pump->configure([ - 'progress' => !$input->getOption('no-progress'), + $batch = $stream->read(); + + $streamBatches = $streamBatches - 1; + $streamedBatches->advance(); + + foreach ($pumps as $key => $pump) { + $pump->setConfig([ + 'skip-pumped' => $input->getOption('skip-pumped'), ]); - $pump->process($data); + $pump->pump($batch); + + $memUsage->setProgress(\memory_get_peak_usage(false)); } - $progress->setProgress($stream->tell()); + $streamed = $stream->tell(); + $streamedRecords->setProgress($streamed); } - $stream->close(); - $progress->finish(); - - $io->writeln("\n"); - $io->success('Data processed successfully!'); + $endSection = new SymfonyStyle($input, $output->section()); + $endSection->success('Data processed successfully!'); return Command::SUCCESS; } + + private function getMemoryLimit(): int + { + $limit = \ini_get('memory_limit'); + if (preg_match('/^(\d+)(.)$/', $limit, $matches)) { + if ($matches[2] == 'M') { + $limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB + } elseif ($matches[2] == 'K') { + $limit = $matches[1] * 1024; // nnnK -> nnn KB + } + } + + return $limit; + } } diff --git a/src/Command/GatewaysSetupCommand.php b/src/Command/GatewaysSetupCommand.php deleted file mode 100644 index 1bb23f6..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 030b3af..06a805b 100644 --- a/src/Command/SystemVarsListCommand.php +++ b/src/Command/SystemVarsListCommand.php @@ -16,14 +16,12 @@ use Symfony\Component\Console\Style\SymfonyStyle; class SystemVarsListCommand extends Command { public function __construct( - private SystemVariableRepository $systemVarRepository + private SystemVariableRepository $systemVarRepository, ) { 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..9ef8c5e 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,13 +19,12 @@ class GatewaysController extends AbstractController public function __construct( private GatewayLocator $gatewayLocator, - ) { - } + ) {} #[Route('/gateway_redirects', name: self::REDIRECT)] public function handleRedirect(Request $request): Response { - $gateway = $this->gatewayLocator->getGateway($request->query->get('gateway')); + $gateway = $this->gatewayLocator->get($request->query->get('gateway')); return $gateway->handleRedirect($request); } @@ -33,7 +32,7 @@ class GatewaysController extends AbstractController #[Route('/gateway_webhooks', name: self::WEBHOOKS)] public function handleWebhook(Request $request): Response { - $gateway = $this->gatewayLocator->getGateway($request->query->get('gateway')); + $gateway = $this->gatewayLocator->get($request->query->get('gateway')); return $gateway->handleWebhook($request); } diff --git a/src/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 9bec1a0..0db83d9 100644 --- a/src/DependencyInjection/SystemVariablesLoader.php +++ b/src/DependencyInjection/SystemVariablesLoader.php @@ -14,9 +14,8 @@ use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceEx final class SystemVariablesLoader implements EnvVarLoaderInterface { public function __construct( - private SystemVariableRepository $systemVarRepository - ) { - } + private SystemVariableRepository $systemVarRepository, + ) {} public function loadEnvVars(): array { diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php deleted file mode 100644 index 85cd70e..0000000 --- a/src/Entity/Accounting.php +++ /dev/null @@ -1,249 +0,0 @@ -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; - } - - 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; - } - - /** - * @return Collection - */ - #[API\ApiProperty(readableLink: false)] - public function getTransactions(): Collection - { - $transactions = [ - ...$this->getTransactionsIssued()->toArray(), - ...$this->getTransactionsReceived()->toArray(), - ]; - - \usort($transactions, function ($a, $b) { - return $a->getId() - $b->getId(); - }); - - return new ArrayCollection($transactions); - } - - /** - * @return Collection - */ - public function getTransactionsIssued(): Collection - { - return $this->transactionsIssued; - } - - public function addTransactionsIssued(AccountingTransaction $transaction): static - { - if (!$this->transactionsIssued->contains($transaction)) { - $this->transactionsIssued->add($transaction); - $transaction->setOrigin($this); - } - - return $this; - } - - public function removeTransactionsIssued(AccountingTransaction $transaction): static - { - if ($this->transactionsIssued->removeElement($transaction)) { - // set the owning side to null (unless already changed) - if ($transaction->getOrigin() === $this) { - $transaction->setOrigin(null); - } - } - - return $this; - } - - /** - * @return Collection - */ - public function getTransactionsReceived(): Collection - { - return $this->transactionsReceived; - } - - public function addTransactionsReceived(AccountingTransaction $transaction): static - { - if (!$this->transactionsReceived->contains($transaction)) { - $this->transactionsReceived->add($transaction); - $transaction->setTarget($this); - } - - return $this; - } - - public function removeTransactionsReceived(AccountingTransaction $transaction): static - { - if ($this->transactionsReceived->removeElement($transaction)) { - // set the owning side to null (unless already changed) - if ($transaction->getTarget() === $this) { - $transaction->setTarget(null); - } - } - - return $this; - } -} diff --git a/src/Entity/Accounting/Accounting.php b/src/Entity/Accounting/Accounting.php new file mode 100644 index 0000000..1695bf7 --- /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 73% rename from src/Entity/AccountingTransaction.php rename to src/Entity/Accounting/Transaction.php index 4688f43..eddf3a0 100644 --- a/src/Entity/AccountingTransaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -1,9 +1,10 @@ addTransactionsIssued($this); - $this->origin = $origin; return $this; @@ -89,8 +84,6 @@ class AccountingTransaction public function setTarget(?Accounting $target): static { - $target->addTransactionsReceived($this); - $this->target = $target; return $this; diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php new file mode 100644 index 0000000..c7d6165 --- /dev/null +++ b/src/Entity/Gateway/Charge.php @@ -0,0 +1,176 @@ + + */ + #[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..8394c43 --- /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 $gatewayName = 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 getGatewayName(): ?string + { + return $this->gatewayName; + } + + public function setGatewayName(string $gatewayName): static + { + $this->gatewayName = $gatewayName; + + 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 5a07536..0000000 --- a/src/Entity/GatewayCharge.php +++ /dev/null @@ -1,105 +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 ceceea7..0000000 --- a/src/Entity/GatewayCheckout.php +++ /dev/null @@ -1,242 +0,0 @@ - 'exact', 'charges.target' => 'exact'])] -#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)] -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)] - #[API\ApiProperty(readableLink: true, writableLink: true)] - #[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; - - /** - * An external identifier provided by the Gateway for the payment.\ - * Required when a GatewayCheckout is completed. - */ - #[API\ApiProperty(writable: false)] - #[API\ApiFilter(SearchFilter::class)] - #[ORM\Column(length: 255, nullable: true)] - private ?string $gatewayReference = null; - - /** - * The URL where the checkout with the Gateway is available. - */ - #[API\ApiProperty(writable: false)] - #[ORM\Column(type: Types::TEXT, nullable: true)] - private ?string $checkoutUrl = null; - - /** - * 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 $migratedReference = 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; - } - - public function getGatewayReference(): ?string - { - return $this->gatewayReference; - } - - public function setGatewayReference(string $gatewayReference): static - { - $this->gatewayReference = $gatewayReference; - - return $this; - } - - public function getCheckoutUrl(): ?string - { - return $this->checkoutUrl; - } - - public function setCheckoutUrl(string $checkoutUrl): static - { - $this->checkoutUrl = $checkoutUrl; - - return $this; - } - - public function isMigrated(): ?bool - { - return $this->migrated; - } - - public function setMigrated(bool $migrated): static - { - $this->migrated = $migrated; - - return $this; - } - - public function getMigratedReference(): ?string - { - return $this->migratedReference; - } - - public function setMigratedReference(?string $migratedReference): static - { - $this->migratedReference = $migratedReference; - - 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 @@ +amount = $amount; $this->currency = $currency; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 35e01a6..1609ad3 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -4,8 +4,11 @@ namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; -use App\Entity\Trait\TimestampableCreationEntity; -use App\Entity\Trait\TimestampableUpdationEntity; +use App\Entity\Accounting\Accounting; +use App\Entity\Interface\AccountingOwnerInterface; +use App\Entity\Trait\MigratedEntity; +use App\Entity\Trait\TimestampedCreationEntity; +use App\Entity\Trait\TimestampedUpdationEntity; use App\Repository\ProjectRepository; use Doctrine\ORM\Mapping as ORM; @@ -23,10 +26,11 @@ use Doctrine\ORM\Mapping as ORM; 'owner' => '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 $migratedReference = 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 getMigratedReference(): ?string - { - return $this->migratedReference; - } - - public function setMigratedReference(?string $migratedReference): static - { - $this->migratedReference = $migratedReference; - - 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 5d0874e..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; @@ -37,10 +40,12 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] -class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface +#[ORM\Index(fields: ['migratedId'])] +class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { - use TimestampableCreationEntity; - use TimestampableUpdationEntity; + use MigratedEntity; + use TimestampedCreationEntity; + use TimestampedUpdationEntity; #[ORM\Id] #[ORM\GeneratedValue] @@ -94,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; /** @@ -124,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 $migratedReference = null; - /** * The projects owned by this User. */ @@ -145,9 +135,18 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class)] private Collection $projects; + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?UserPersonal $personalData = null; + public function __construct() { - $this->migrated = false; + $accounting = new Accounting(); + $accounting->setOwner($this); + + $this->accounting = $accounting; + + $this->emailConfirmed = false; + $this->active = false; $this->tokens = new ArrayCollection(); $this->projects = new ArrayCollection(); @@ -285,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; @@ -358,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 getMigratedReference(): ?string - { - return $this->migratedReference; - } - - public function setMigratedReference(?string $migratedReference): static - { - $this->migratedReference = $migratedReference; - - return $this; - } - /** * @return Collection */ @@ -411,4 +386,21 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs return $this; } + + public function getPersonalData(): ?UserPersonal + { + return $this->personalData; + } + + public function setPersonalData(UserPersonal $personalData): static + { + // set the owning side of the relation if necessary + if ($personalData->getUser() !== $this) { + $personalData->setUser($this); + } + + $this->personalData = $personalData; + + return $this; + } } diff --git a/src/Entity/UserPersonal.php b/src/Entity/UserPersonal.php new file mode 100644 index 0000000..13e86fc --- /dev/null +++ b/src/Entity/UserPersonal.php @@ -0,0 +1,92 @@ +user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getIdentityDocument(): ?string + { + return $this->identityDocument; + } + + public function setIdentityDocument(?string $identityDocument): static + { + $this->identityDocument = $identityDocument; + + return $this; + } + + public function getIndetityDocumentType(): ?string + { + return $this->identityDocumentType; + } + + public function setIndetityDocumentType(?string $identityDocumentType): static + { + $this->identityDocumentType = $identityDocumentType; + + return $this; + } + + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + public function setPostalCode(?string $postalCode): static + { + $this->postalCode = $postalCode; + + return $this; + } +} 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..34f7ddc --- /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..be15c70 --- /dev/null +++ b/src/Entity/WalletStatement.php @@ -0,0 +1,177 @@ + + */ + #[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..d8c845a --- /dev/null +++ b/src/EventListener/WalletTransactionsListener.php @@ -0,0 +1,43 @@ +getTarget()->getOwner() instanceof User) { + return; + } + + $income = $this->wallet->save($transaction); + + if ($income->getId() !== null) { + return; + } + + $event->getObjectManager()->persist($income); + $event->getObjectManager()->flush(); + } +} diff --git a/src/Filter/OrderedLikeFilter.php b/src/Filter/OrderedLikeFilter.php index 856e044..27197ce 100644 --- a/src/Filter/OrderedLikeFilter.php +++ b/src/Filter/OrderedLikeFilter.php @@ -17,7 +17,7 @@ final class OrderedLikeFilter extends AbstractFilter protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, - protected ?NameConverterInterface $nameConverter = null + protected ?NameConverterInterface $nameConverter = null, ) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); } @@ -29,7 +29,7 @@ final class OrderedLikeFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { if ( !$this->isPropertyEnabled($property, $resourceClass) diff --git a/src/Filter/ResourceVersionResourceFilter.php b/src/Filter/ResourceVersionResourceFilter.php index a331e12..3fddcdf 100644 --- a/src/Filter/ResourceVersionResourceFilter.php +++ b/src/Filter/ResourceVersionResourceFilter.php @@ -31,7 +31,7 @@ final class ResourceVersionResourceFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { return; } diff --git a/src/Filter/ResourceVersionResourceIdFilter.php b/src/Filter/ResourceVersionResourceIdFilter.php index f8a9804..e7521bc 100644 --- a/src/Filter/ResourceVersionResourceIdFilter.php +++ b/src/Filter/ResourceVersionResourceIdFilter.php @@ -17,7 +17,7 @@ final class ResourceVersionResourceIdFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { return; } diff --git a/src/Filter/UserQueryFilter.php b/src/Filter/UserQueryFilter.php index 07409c0..e179cb3 100644 --- a/src/Filter/UserQueryFilter.php +++ b/src/Filter/UserQueryFilter.php @@ -17,7 +17,7 @@ final class UserQueryFilter extends AbstractFilter protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, - protected ?NameConverterInterface $nameConverter = null + protected ?NameConverterInterface $nameConverter = null, ) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); } @@ -29,7 +29,7 @@ final class UserQueryFilter extends AbstractFilter QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, - array $context = [] + array $context = [], ): void { if ( !$this->isPropertyEnabled($property, $resourceClass) diff --git a/src/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 @@ $gateway) { + $this->gatewaysByClass[$gateway::class] = $gateway; + } + + self::validateGatewayNames($this->gatewaysByClass); + + foreach ($this->gatewaysByClass as $class => $gateway) { + $this->gatewaysByName[$gateway::getName()] = $gateway; + } + } + + /** + * @return + */ + public function getAll(): array + { + return $this->gatewaysByName; + } + + /** + * @param string $name Name of the Gateway interface implementation + * + * @throws \Exception When the `$name` does not match to that of an implemented Gateway + */ + public function get(string $name): GatewayInterface + { + if (!\array_key_exists($name, $this->gatewaysByName)) { + throw new MissingGatewayException($name); + } + + return $this->gatewaysByName[$name]; + } + + /** + * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway + */ + public function getForCheckout(Checkout $checkout): GatewayInterface + { + return $this->get($checkout->getGatewayName()); + } + + /** + * 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)) { + throw new DuplicateGatewayException( + $gatewayName, + $gatewayClass::class, + $gatewaysValidated[$gatewayName]::class + ); + } + + $gatewaysValidated[$gatewayName] = $gatewayClass; + } + } +} diff --git a/src/Gateway/Link.php b/src/Gateway/Link.php new file mode 100644 index 0000000..40cb124 --- /dev/null +++ b/src/Gateway/Link.php @@ -0,0 +1,30 @@ +paypal->postOrder([ + 'intent' => self::PAYPAL_ORDER_INTENT, + 'purchase_units' => $this->getPaypalPurchaseUnits($checkout), + 'payment_source' => $this->getPaypalPaymentSource($checkout), + ]); + + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_ORDER; + $tracking->value = $order['id']; + + $checkout->addTracking($tracking); + + foreach ($order['links'] as $linkData) { + $linkType = \in_array($linkData['rel'], ['approve', 'payer-action']) + ? LinkType::Payment + : LinkType::Debug; + + $link = new Link(); + $link->href = $linkData['href']; + $link->rel = $linkData['rel']; + $link->method = $linkData['method']; + $link->type = $linkType; + + $checkout->addLink($link); + } + + return $checkout; + } + + public function handleRedirect(Request $request): RedirectResponse + { + if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) { + throw new \Exception(sprintf('Checkout was not completed successfully.')); + } + + $checkoutId = $request->query->get('checkoutId'); + + $checkout = $this->checkoutRepository->find($checkoutId); + if ($checkout === null) { + throw new \Exception(sprintf("Checkout '%s' could not be found.", $checkoutId)); + } + + if ($checkout->getStatus() === CheckoutStatus::Charged) { + return new RedirectResponse($this->iriConverter->getIriFromResource($checkout)); + } + + $orderId = $request->query->get('token'); + if (!$orderId) { + throw new \Exception(sprintf('PayPal checkout order ID not provided by the gateway.')); + } + + $order = $this->paypal->getOrder($orderId); + if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) { + throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId)); + } + + $capture = $this->paypal->captureOrderPayment($order); + if ($capture['status'] !== self::PAYPAL_STATUS_COMPLETED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId)); + } + + foreach ($capture['purchase_units'] as $purchaseUnit) { + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + + $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)); + } + + public function handleWebhook(Request $request): Response + { + try { + $event = $this->paypal->verifyWebhook($request); + } catch (\Exception $e) { + return new JsonResponse([ + 'status' => 'ERROR', + 'message' => $e->getMessage(), + 'requestBody' => $request->getContent(), + 'requestHeaders' => $request->headers->all(), + ], Response::HTTP_ACCEPTED); + } + + switch ($event['event_type']) { + case self::PAYPAL_EVENT_ORDER_COMPLETED: + return $this->handleOrderCompleted($event); + default: + return new Response('Event not supported', Response::HTTP_ACCEPTED); + } + + return new Response(); + } + + private function handleOrderCompleted(array $event) + { + $orderId = $event['resource']['id']; + + $checkout = $this->checkoutRepository->findOneByTracking(self::TRACKING_TITLE_ORDER, $orderId); + + if ($checkout === null) { + throw new \Exception(sprintf("Could not find any Checkout by the Tracking '%s'", $orderId), 1); + } + + foreach ($event['resource']['purchase_units'] as $purchaseUnit) { + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_TRANSACTION; + $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + + $checkout->addTracking($tracking); + } + + $checkout = $this->checkoutService->chargeCheckout($checkout); + } + + private function getPaypalMoney(Charge $charge): array + { + $brick = BrickMoney::ofMinor( + $charge->getMoney()->amount, + $charge->getMoney()->currency + ); + + return [ + 'value' => $brick->getAmount()->__toString(), + 'currency_code' => $brick->getCurrency()->getCurrencyCode(), + ]; + } + + private function getPaypalPurchaseUnits(Checkout $checkout): array + { + $units = []; + + foreach ($checkout->getCharges() as $charge) { + $money = $this->getPaypalMoney($charge); + $reference = $this->checkoutService->generateTracking($checkout, $charge); + + $units[] = [ + 'reference_id' => $reference, + 'custom_id' => $reference, + 'items' => [ + [ + 'name' => $charge->getTitle(), + 'description' => $charge->getDescription(), + 'quantity' => '1', + 'unit_amount' => [ + ...$money, + ], + ], + ], + 'amount' => [ + ...$money, + 'breakdown' => [ + 'item_total' => [ + ...$money, + ], + ], + ], + ]; + } + + return $units; + } + + private function getPaypalPaymentSource(Checkout $checkout): array + { + return [ + 'paypal' => [ + 'experience_context' => [ + 'return_url' => $this->checkoutService->generateRedirectUrl($checkout), + ], + ], + ]; + } +} diff --git a/src/Gateway/Paypal/PaypalService.php b/src/Gateway/Paypal/PaypalService.php new file mode 100644 index 0000000..e826d05 --- /dev/null +++ b/src/Gateway/Paypal/PaypalService.php @@ -0,0 +1,178 @@ +cache = new FilesystemAdapter(\preg_replace( + '/[^-+_\.A-Za-z0-9]/', + '-', + $this->paypalApiAddress + )); + + $httpOptions = new HttpOptions(); + $httpOptions->setBaseUri($paypalApiAddress); + + $this->setHttpClient($httpClient->withOptions($httpOptions->toArray())); + } + + public function setHttpClient(HttpClientInterface $httpClient): static + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Calls the PayPal API to generate an OAuth2 token. + * + * @return array The API response body + */ + public function generateAuthToken(): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ + 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], + 'body' => 'grant_type=client_credentials', + ]); + + return $response->toArray(); + } + + /** + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. + * + * @return array The OAuth2 token data + */ + public function getAuthToken(): array + { + return $this->cache->get(urlencode('/v1/oauth2/token'), function (ItemInterface $item): array { + $tokenData = $this->generateAuthToken(); + + $item->expiresAfter($tokenData['expires_in']); + + return $tokenData; + }); + } + + /** + * Creates a PayPal Order resource with the given data. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ + public function postOrder(array $order): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'json' => $order, + ]); + + $content = \json_decode($response->getContent(), true); + if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) { + throw new \Exception($content['message']); + } + + return $content; + } + + /** + * Retrieve a PayPal Order resource. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_get + */ + public function getOrder(string $orderId): array + { + $request = $this->httpClient->request( + Request::METHOD_GET, + sprintf('/v2/checkout/orders/%s', $orderId), + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); + } + + return \json_decode($request->getContent(), true); + } + + /** + * Process post user-approval payment capture for a given Order. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ + public function captureOrderPayment(array $order): array + { + $link = \array_filter($order['links'], function ($order) { + return $order['rel'] === 'capture'; + }); + + if (empty($link)) { + throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); + } + + $link = \array_pop($link); + $request = $this->httpClient->request( + $link['method'], + $link['href'], + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'headers' => ['Content-Type' => 'application/json'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_CREATED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); + } + + return \json_decode($request->getContent(), true); + } + + /** + * Verifies a PayPal webhook request. + * + * @param Request $request The webhook request + * + * @return array The webhook event data + * + * @throws \Exception If the verification fails + */ + public function verifyWebhook(Request $request): array + { + $headers = $request->headers; + + $isSignatureValid = \openssl_verify( + implode('|', [ + $headers->get('paypal-transmission-id'), + $headers->get('paypal-transmission-type'), + $this->paypalWebhookId, + \crc32($request->getContent()), + ]), + \base64_decode($headers->get('paypal-transmission-sig')), + \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) + ) === 1; + + if (!$isSignatureValid) { + throw new \Exception('Could not verify PayPal webhook signature.'); + } + + return \json_decode($request->getContent(), true); + } +} diff --git a/src/Library/Economy/Payment/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php similarity index 57% rename from src/Library/Economy/Payment/StripeGateway.php rename to src/Gateway/Stripe/StripeGateway.php index 7bf9f81..dc69356 100644 --- a/src/Library/Economy/Payment/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -1,14 +1,18 @@ stripe = new StripeClient($stripeApiKey); } @@ -41,52 +44,41 @@ class StripeGateway implements GatewayInterface return 'stripe'; } - public function sendData(GatewayCheckout $checkout): GatewayCheckout + public static function getSupportedChargeTypes(): array { - $successUrl = $this->router->generate( - GatewaysController::REDIRECT, - [ - 'type' => self::RESPONSE_TYPE_SUCCESS, - 'gateway' => $this->getName(), - ], - RouterInterface::ABSOLUTE_URL - ); + return [ + ChargeType::Single, + ChargeType::Recurring, + ]; + } + public function process(Checkout $checkout): Checkout + { $session = $this->stripe->checkout->sessions->create([ - 'customer_email' => $checkout->getOrigin()->getUser()->getEmail(), + 'customer_email' => $this->getStripeCustomer($checkout), 'mode' => $this->getStripeMode($checkout), 'line_items' => $this->getStripeLineItems($checkout), // Because Symfony's Router encodes query parameters, the value {CHECKOUT_SESSION_ID} // is not properly sent to Stripe and the redirection fails, // that's why we add the session_id template variable like this. // https://docs.stripe.com/payments/checkout/custom-success-page?lang=php#modify-the-success-url - 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $successUrl), + 'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)), ]); - $checkout->setCheckoutUrl($session->url); - $checkout->setGatewayReference($session->id); + $link = new Link(); - return $checkout; - } + $link->href = $session->url; + $link->rel = 'approve'; + $link->method = Request::METHOD_GET; + $link->type = LinkType::Payment; - private function handleSuccess(GatewayCheckout $checkout): GatewayCheckout - { - $checkout->setStatus(GatewayCheckoutStatus::Charged); + $checkout->addLink($link); - foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); + $tracking = new Tracking(); + $tracking->title = self::TRACKING_TITLE_CHECKOUT; + $tracking->value = $session->id; - $this->entityManager->persist($transaction); - - $charge->setTransaction($transaction); - - $this->entityManager->persist($charge); - } - - $this->entityManager->flush(); + $checkout->addTracking($tracking); return $checkout; } @@ -96,15 +88,17 @@ class StripeGateway implements GatewayInterface $sessionId = $request->query->get('session_id'); $session = $this->stripe->checkout->sessions->retrieve($sessionId); - $checkout = $this->gatewayCheckoutRepository->findOneBy( - ['gatewayReference' => $sessionId] - ); + $checkout = $this->checkoutRepository->find($request->query->get('checkoutId')); if ($checkout === null) { - throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId)); + throw new \Exception(sprintf("Stripe checkout '%s' exists but no Checkout with that reference was found.", $sessionId)); } - if ($request->query->get('type') !== self::RESPONSE_TYPE_SUCCESS) { + if ($checkout->getStatus() === CheckoutStatus::Charged) { + return $checkout; + } + + if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) { return $checkout; } @@ -112,7 +106,10 @@ class StripeGateway implements GatewayInterface return $checkout; } - $checkout = $this->handleSuccess($checkout); + $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)); @@ -135,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; } } @@ -146,7 +153,7 @@ class StripeGateway implements GatewayInterface return StripeSession::MODE_PAYMENT; } - private function getStripeLineItems(GatewayCheckout $checkout): array + private function getStripeLineItems(Checkout $checkout): array { $items = []; @@ -154,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/Gateway/Tracking.php b/src/Gateway/Tracking.php new file mode 100644 index 0000000..69c7226 --- /dev/null +++ b/src/Gateway/Tracking.php @@ -0,0 +1,20 @@ +getOrigin(); + $available = $this->wallet->getBalance($origin); + + if ($this->money->isLess($available, $this->getChargeTotal($checkout))) { + 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()); + + $expenditure = $this->wallet->spend($transaction); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); + } + + $checkout = $this->checkoutService->chargeCheckout($checkout); + + 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..66c8428 --- /dev/null +++ b/src/Gateway/Wallet/WalletService.php @@ -0,0 +1,126 @@ +statementRepository->findByAccounting($accounting); + } + + public function getBalance(Accounting $accounting): Money + { + $total = Brick\Money::ofMinor(0, $accounting->getCurrency()); + $statements = $this->statementRepository->findByTarget($accounting); + + foreach ($statements as $statement) { + $balance = $this->money->toBrick($statement->getBalance()); + + $total = $total->plus($balance); + } + + return $this->money->toMoney($total); + } + + /** + * Obtain an `incoming` WalletStatement for the Transaction. + * + * @param Transaction $transaction The Transaction targetting a wallet + * + * @return WalletStatement An incoming statement for the transaction + */ + public function save(Transaction $transaction): WalletStatement + { + $statement = $this->statementRepository->findByTransaction($transaction); + + if ($statement) { + return $statement; + } + + $statement = new WalletStatement(); + $statement->setTransaction($transaction); + $statement->setDirection(StatementDirection::Incoming); + $statement->setBalance($transaction->getMoney()); + + 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); + + $spendGoal = $this->money->substract($balanceSpent, $spendGoal); + $spentTotal = $this->money->add($balanceSpent, $spentTotal); + } + + 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/Benzina.php b/src/Library/Benzina/Benzina.php index 881ef84..99fdede 100644 --- a/src/Library/Benzina/Benzina.php +++ b/src/Library/Benzina/Benzina.php @@ -11,7 +11,7 @@ class Benzina private array $availablePumps = []; public function __construct( - iterable $instanceof + iterable $instanceof, ) { $this->availablePumps = \iterator_to_array($instanceof); } @@ -23,7 +23,7 @@ class Benzina * * @return PumpInterface[] */ - public function getPumps(StreamInterface $stream, int $sampleSize = 1): array + public function getPumpsFor(StreamInterface $stream, int $sampleSize = 1): array { $sample = $stream->read($sampleSize); $stream->rewind(); diff --git a/src/Library/Benzina/Pump/AbstractPump.php b/src/Library/Benzina/Pump/AbstractPump.php index b349421..3775fe7 100644 --- a/src/Library/Benzina/Pump/AbstractPump.php +++ b/src/Library/Benzina/Pump/AbstractPump.php @@ -4,10 +4,21 @@ namespace App\Library\Benzina\Pump; abstract class AbstractPump implements PumpInterface { - protected array $config; + protected array $config = []; - public function configure(array $config = []): void + public function setConfig(array $config = []): void { $this->config = $config; } + + public function getConfig(?string $key = null, mixed $default = null): mixed + { + if ($key !== null) { + 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 db37971..cf54a5b 100644 --- a/src/Library/Benzina/Pump/CheckoutsPump.php +++ b/src/Library/Benzina/Pump/CheckoutsPump.php @@ -2,34 +2,42 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; -use App\Entity\AccountingTransaction; -use App\Entity\GatewayCharge; -use App\Entity\GatewayChargeType; -use App\Entity\GatewayCheckout; -use App\Entity\GatewayCheckoutStatus; +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\ProgressivePumpTrait; -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\Library\Benzina\Pump\Trait\DoctrinePumpTrait; use App\Repository\ProjectRepository; use App\Repository\TipjarRepository; use App\Repository\UserRepository; +use App\Service\Gateway\CheckoutService; use Doctrine\ORM\EntityManagerInterface; class CheckoutsPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; + + public const TRACKING_TITLE_V3 = 'v3 Invest ID'; + public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment'; + public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction'; + 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 CHECKOUT_URL_DEFAULT = 'https://www.goteo.org/invest'; private const MAX_INT = 2147483647; @@ -69,34 +77,30 @@ class CheckoutsPump extends AbstractPump implements PumpInterface private UserRepository $userRepository, private ProjectRepository $projectRepository, private TipjarRepository $tipjarRepository, - private EntityManagerInterface $entityManager - ) { - } + private EntityManagerInterface $entityManager, + private CheckoutService $checkoutService, + ) {} - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\array_key_exists(0, $data)) { + if (!\is_array($batch) || !\array_key_exists(0, $batch)) { return false; } - return $this->hasAllKeys($data[0], self::INVEST_KEYS); + return $this->hasAllKeys($batch[0], self::INVEST_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $users = $this->getUsers($data); - $projects = $this->getProjects($data); + $batch = $this->skipPumped($batch, 'id', Checkout::class, 'migratedId'); $tipjar = $this->getPlatformTipjar(); - $pumped = $this->getPumped(GatewayCheckout::class, $data, ['migratedReference' => 'id']); + $users = $this->getPumpedUsers($batch); + $projects = $this->getPumpedProjects($batch); - foreach ($data as $key => $record) { - if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { - continue; - } - - if (!\array_key_exists($record['project'], $projects)) { + foreach ($batch as $key => $record) { + if (!$record['user'] || empty($record['user'])) { continue; } @@ -109,54 +113,52 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } $user = $users[$record['user']]; - $project = $projects[$record['project']]; - $checkout = new GatewayCheckout(); + $checkout = new Checkout(); $checkout->setOrigin($user->getAccounting()); $checkout->setStatus($this->getCheckoutStatus($record)); - $checkout->setGateway($this->getCheckoutGateway($record)); - $checkout->setGatewayReference($this->getCheckoutReference($record)); - $checkout->setCheckoutUrl(self::CHECKOUT_URL_DEFAULT); + $checkout->setGatewayName($this->getCheckoutGateway($record)); + + foreach ($this->getCheckoutTrackings($record) as $tracking) { + $checkout->addTracking($tracking); + } + $checkout->setMigrated(true); - $checkout->setMigratedReference($record['id']); - $checkout->setMetadata([ - 'payment' => $record['payment'], - 'transaction' => $record['transaction'], - 'preapproval' => $record['preapproval'], - ]); + $checkout->setMigratedId($record['id']); $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'])); - $charge->setTarget($project->getAccounting()); + + 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()); - $this->entityManager->persist($tip); $checkout->addCharge($tip); } - $this->entityManager->persist($charge); - $checkout->addCharge($charge); - - if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) { - foreach ($checkout->getCharges() as $charge) { - $transaction = new AccountingTransaction(); - $transaction->setMoney($charge->getMoney()); - $transaction->setOrigin($checkout->getOrigin()); - $transaction->setTarget($charge->getTarget()); - - $charge->setTransaction($transaction); - - $this->entityManager->persist($transaction); - $this->entityManager->persist($charge); - } + if ($checkout->getStatus() === CheckoutStatus::Charged) { + $checkout = $this->checkoutService->chargeCheckout($checkout); } $this->entityManager->persist($checkout); @@ -166,32 +168,38 @@ class CheckoutsPump extends AbstractPump implements PumpInterface $this->entityManager->clear(); } - private function getUsers(array $data): array + /** + * @return array + */ + private function getPumpedUsers(array $batch): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['user']; - }, $data)]); + $users = $this->userRepository->findBy(['migratedId' => \array_map(function ($batch) { + return $batch['user']; + }, $batch)]); - $usersByMigratedReference = []; + $usersByMigratedId = []; foreach ($users as $user) { - $usersByMigratedReference[$user->getMigratedReference()] = $user; + $usersByMigratedId[$user->getMigratedId()] = $user; } - return $usersByMigratedReference; + return $usersByMigratedId; } - private function getProjects(array $data): array + /** + * @return array + */ + private function getPumpedProjects(array $batch): array { - $projects = $this->projectRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['project']; - }, $data)]); + $projects = $this->projectRepository->findBy(['migratedId' => \array_map(function ($batch) { + return $batch['project']; + }, $batch)]); - $projectsByMigratedReference = []; + $projectsByMigratedId = []; foreach ($projects as $project) { - $projectsByMigratedReference[$project->getMigratedReference()] = $project; + $projectsByMigratedId[$project->getMigratedId()] = $project; } - return $projectsByMigratedReference; + return $projectsByMigratedId; } private function getPlatformTipjar(): Tipjar @@ -205,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 @@ -235,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 @@ -264,20 +268,41 @@ class CheckoutsPump extends AbstractPump implements PumpInterface } } - private function getCheckoutReference(array $record): string + /** + * @return Tracking[] + */ + private function getCheckoutTrackings(array $record): array { + $v3Tracking = new Tracking(); + $v3Tracking->title = self::TRACKING_TITLE_V3; + $v3Tracking->value = $record['id']; + + $trackings = [$v3Tracking]; + if (!empty($record['payment'])) { - return $record['payment']; + $payment = new Tracking(); + $payment->title = self::TRACKING_TITLE_PAYMENT; + $payment->value = $record['payment']; + + $trackings[] = $payment; } if (!empty($record['transaction'])) { - return $record['transaction']; + $transaction = new Tracking(); + $transaction->title = self::TRACKING_TITLE_TRANSACTION; + $transaction->value = $record['transaction']; + + $trackings[] = $transaction; } if (!empty($record['preapproval'])) { - return $record['preapproval']; + $preapproval = new Tracking(); + $preapproval->title = self::TRACKING_TITLE_PREAPPROVAL; + $preapproval->value = $record['preapproval']; + + $trackings[] = $preapproval; } - return ''; + return $trackings; } } diff --git a/src/Library/Benzina/Pump/ProjectsPump.php b/src/Library/Benzina/Pump/ProjectsPump.php index 62538c9..dd0edb9 100644 --- a/src/Library/Benzina/Pump/ProjectsPump.php +++ b/src/Library/Benzina/Pump/ProjectsPump.php @@ -2,19 +2,19 @@ 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; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; -use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait; +use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; class ProjectsPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; private const PROJECT_KEYS = [ 'id', @@ -96,31 +96,25 @@ class ProjectsPump extends AbstractPump implements PumpInterface public function __construct( private UserRepository $userRepository, - private EntityManagerInterface $entityManager - ) { - } + private EntityManagerInterface $entityManager, + ) {} - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\array_key_exists(0, $data)) { + if (!\is_array($batch) || !\array_key_exists(0, $batch)) { return false; } - return $this->hasAllKeys($data[0], self::PROJECT_KEYS); + return $this->hasAllKeys($batch[0], self::PROJECT_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $pumped = $this->getPumped(Project::class, $data, ['migratedReference' => 'id']); - $owners = $this->getOwners($data); + $batch = $this->skipPumped($batch, 'id', Project::class, 'migratedId'); - foreach ($data as $key => $record) { - $isPumped = $this->isPumped($record, $pumped, ['migratedReference' => 'id']); - - if ($isPumped) { - continue; - } + $owners = $this->getOwners($batch); + foreach ($batch as $key => $record) { if (!\array_key_exists($record['owner'], $owners)) { continue; } @@ -134,14 +128,11 @@ class ProjectsPump extends AbstractPump implements PumpInterface $project->setOwner($owners[$record['owner']]); $project->setStatus($this->getProjectStatus($record['status'])); $project->setMigrated(true); - $project->setMigratedReference($record['id']); + $project->setMigratedId($record['id']); $project->setDateCreated(new \DateTime($record['created'])); - - $accounting = $this->getAccounting($record); - $accounting->setProject($project); + $project->setDateUpdated(new \DateTime()); $this->entityManager->persist($project); - $this->entityManager->persist($accounting); } $this->entityManager->flush(); @@ -151,15 +142,15 @@ class ProjectsPump extends AbstractPump implements PumpInterface /** * @return User[] */ - private function getOwners(array $data): array + private function getOwners(array $record): array { - $users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) { - return $data['owner']; - }, $data)]); + $users = $this->userRepository->findBy(['migratedId' => \array_map(function ($record) { + return $record['owner']; + }, $record)]); $owners = []; foreach ($users as $user) { - $owners[$user->getMigratedReference()] = $user; + $owners[$user->getMigratedId()] = $user; } return $owners; diff --git a/src/Library/Benzina/Pump/PumpInterface.php b/src/Library/Benzina/Pump/PumpInterface.php index f071335..7dbb670 100644 --- a/src/Library/Benzina/Pump/PumpInterface.php +++ b/src/Library/Benzina/Pump/PumpInterface.php @@ -4,24 +4,34 @@ namespace App\Library\Benzina\Pump; interface PumpInterface { - /** - * Determines if the data is supported by this pump. - * - * @param mixed $data The streamed records, e.g. rows from an user table - */ - public function supports(mixed $data): bool; - /** * Sets flexible configuration values for this pump. * - * @param array $config the configuration array + * @param array $config The configuration array */ - public function configure(array $config = []): void; + public function setConfig(array $config = []): void; /** - * Process the data to be pumped. + * Read the configuration values for this pump. * - * @param mixed $data The streamed records, e.g. rows from an user table + * @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 value at the specified key, or full keys and values if null */ - public function process(mixed $data): void; + public function getConfig(?string $key = null, mixed $default = null): mixed; + + /** + * Determines if a data batch is supported by this pump. + * + * @param mixed $batch A sample of the streamed records, e.g. rows from an user table + */ + public function supports(mixed $batch): bool; + + /** + * Pump a data batch into a final destination. + * + * @param mixed $batch The streamed records, e.g. rows from an user table + */ + public function pump(mixed $batch): void; } diff --git a/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php new file mode 100644 index 0000000..61e2e78 --- /dev/null +++ b/src/Library/Benzina/Pump/Trait/DoctrinePumpTrait.php @@ -0,0 +1,55 @@ +getConfig('skip-pumped')) { + return $batch; + } + + $pumped = $this->entityManager + ->getRepository($entityClass) + ->findBy([$entityKey => \array_map(function (array $record) use ($batchKey) { + return $record[$batchKey]; + }, $batch)]); + + $pumpedKeys = []; + foreach ($pumped as $key => $entity) { + $entityKeyGetter = sprintf('get%s', \ucfirst($entityKey)); + + $pumpedKeys[] = $entity->$entityKeyGetter(); + } + + return \array_filter($batch, function (array $record) use ($batchKey, $pumpedKeys) { + return !\in_array($record[$batchKey], $pumpedKeys); + }); + } +} diff --git a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php b/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php deleted file mode 100644 index 920648f..0000000 --- a/src/Library/Benzina/Pump/Trait/ProgressivePumpTrait.php +++ /dev/null @@ -1,103 +0,0 @@ -getPumped(Entity::class, $data, ['entityId' => 'dataId']); - * ... - * if ($this->isPumped($record, $alreadyPumped)) continue; - * ``` - */ -trait ProgressivePumpTrait -{ - public function __construct( - private EntityManagerInterface $entityManager - ) { - } - - /** - * Retrieve already pumped data from the pumping data batch. - * - * @param string $entityClass The class to pump the data to - * @param array $pumpingBatch The pumping data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function getPumped( - string $entityClass, - array $pumpingBatch, - array $matchCriteria - ): array { - $repository = $this->entityManager->getRepository($entityClass); - - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - $pumped = $repository->findBy([$entityKey => \array_map(function ($data) use ($pumpingKey) { - return $data[$pumpingKey]; - }, $pumpingBatch)]); - - $pumpedByKey = []; - foreach ($pumped as $pumped) { - $getter = sprintf('get%s', ucfirst($entityKey)); - $pumpedByKey[$pumped->$getter()] = $pumped; - } - - return $pumpedByKey; - } - - /** - * Determine if a pumping data record is already in a pumped data batch. - * - * @param array $pumpingRecord The data record to be pumped - * @param array $pumpedBatch The pumped data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function isPumped( - array $pumpingRecord, - array $pumpedBatch, - array $matchCriteria - ): bool { - if ( - isset($this->config) - && $this->config['progress'] === false - && \array_key_exists('progress', $this->config) - ) { - return false; - } - - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - return \array_key_exists($pumpingRecord[$pumpingKey], $pumpedBatch); - } - - /** - * Obtain the pumped record in a pumped data batch from a pumping data record. - * - * @param array $pumpingRecord The data record being pumped - * @param array $pumpedBatch The pumped data batch - * @param array $matchCriteria An array with the matching criteria between the entity key and the data key - */ - public function getPumpedRecord( - array $pumpingRecord, - array $pumpedBatch, - array $matchCriteria - ): mixed { - $entityKey = \array_keys($matchCriteria)[0]; - $pumpingKey = $matchCriteria[$entityKey]; - - if ($this->isPumped($pumpingRecord, $pumpedBatch, $matchCriteria)) { - return $pumpedBatch[$pumpingRecord[$pumpingKey]]; - } - - return false; - } -} diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index def0707..ae5f8d7 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -2,16 +2,15 @@ namespace App\Library\Benzina\Pump; -use App\Entity\Accounting; use App\Entity\User; use App\Library\Benzina\Pump\Trait\ArrayPumpTrait; -use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait; +use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait; use Doctrine\ORM\EntityManagerInterface; class UsersPump extends AbstractPump implements PumpInterface { use ArrayPumpTrait; - use ProgressivePumpTrait; + use DoctrinePumpTrait; public const USER_KEYS = [ 'id', @@ -51,30 +50,29 @@ class UsersPump extends AbstractPump implements PumpInterface ]; public function __construct( - private EntityManagerInterface $entityManager - ) { - } + private EntityManagerInterface $entityManager, + ) {} - public function supports(mixed $data): bool + public function supports(mixed $batch): bool { - if (!\is_array($data) || !\is_array($data[0])) { + if (!\is_array($batch) || !\is_array($batch[0])) { return false; } - return $this->hasAllKeys($data[0], self::USER_KEYS); + return $this->hasAllKeys($batch[0], self::USER_KEYS); } - public function process(mixed $data): void + public function pump(mixed $batch): void { - $pumped = $this->getPumped(User::class, $data, ['migratedReference' => 'id']); + $batch = $this->skipPumped($batch, 'id', User::class, 'migratedId'); - foreach ($data as $key => $record) { - if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) { - continue; + 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']); @@ -82,13 +80,11 @@ class UsersPump extends AbstractPump implements PumpInterface $user->setName($record['name']); $user->setActive(false); $user->setMigrated(true); - $user->setMigratedReference($record['id']); - - $accounting = new Accounting(); - $accounting->setUser($user); + $user->setMigratedId($record['id']); + $user->setDateCreated($created); + $user->setDateUpdated(new \DateTime()); $this->entityManager->persist($user); - $this->entityManager->persist($accounting); } $this->entityManager->flush(); @@ -113,12 +109,12 @@ class UsersPump extends AbstractPump implements PumpInterface return $username; } - private function getUsername(array $data): string + private function getUsername(array $record): string { - $username = $this->normalizeUsername($data['id']); + $username = $this->normalizeUsername($record['id']); if (!$username) { - $username = $this->normalizeUsername($data['email']); + $username = $this->normalizeUsername($record['email']); } return $username; diff --git a/src/Library/Benzina/Stream/PdoStream.php b/src/Library/Benzina/Stream/PdoStream.php index 1b8dd11..ad8c40f 100644 --- a/src/Library/Benzina/Stream/PdoStream.php +++ b/src/Library/Benzina/Stream/PdoStream.php @@ -13,7 +13,7 @@ class PdoStream implements StreamInterface public function __construct( string $database, private string $tablename, - private int $sizeOfBatch = 99 + private int $sizeOfBatch = 99, ) { $parsedUrl = parse_url($database); $dbdata = [ diff --git a/src/Library/Economy/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/GatewayLocator.php b/src/Library/Economy/Payment/GatewayLocator.php deleted file mode 100644 index 5c80a56..0000000 --- a/src/Library/Economy/Payment/GatewayLocator.php +++ /dev/null @@ -1,89 +0,0 @@ - $gateway) { - $this->gatewaysByClass[$gateway::class] = $gateway; - } - - GatewaysCompilerPass::validateGatewayNames($this->gatewaysByClass); - - foreach ($this->gatewaysByClass as $class => $gateway) { - $this->gatewaysByName[$gateway::getName()] = $gateway; - } - } - - /** - * @return array List of the fully-qualified class names of the available interfaces - */ - public function getClasses(): array - { - return \array_keys($this->gatewaysByClass); - } - - /** - * @return array List of names of the available interfaces - */ - public function getNames(): array - { - return \array_keys($this->gatewaysByName); - } - - /** - * @return 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[] - */ - public function getGateways(): array - { - return $this->gatewaysByName; - } - - /** - * @param string $name Name of the Gateway interface implementation - * - * @throws \Exception When the $name does not match to that of an implemented Gateway - */ - public function getGateway(string $name): GatewayInterface - { - if (!\array_key_exists($name, $this->gatewaysByName)) { - throw new \Exception("No such Gateway with the name $name"); - } - - return $this->gatewaysByName[$name]; - } - - /** - * @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway - */ - public function getGatewayOf(GatewayCheckout $checkout): GatewayInterface - { - $gateway = $checkout->getGateway(); - if (!$gateway) { - throw new \Exception('The given GatewayCheckout does not specify a Gateway'); - } - - return $this->getGateway($gateway); - } -} diff --git a/src/Library/Economy/Payment/PaypalGateway.php b/src/Library/Economy/Payment/PaypalGateway.php deleted file mode 100644 index 934ecad..0000000 --- a/src/Library/Economy/Payment/PaypalGateway.php +++ /dev/null @@ -1,11 +0,0 @@ -getOwner(); + + $resource = new Resource\Accounting(); + $resource->id = $entity->getId(); + $resource->currency = $entity->getCurrency(); + $resource->owner = $owner; + $resource->balance = $this->getBalance($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; + } + + private function getBalance(AccountingOwnerInterface $owner): Money + { + if ($owner instanceof User) { + return $this->wallet->getBalance($owner->getAccounting()); + } + + $accounting = $owner->getAccounting(); + + $balance = new Money(0, $accounting->getCurrency()); + $transactions = $this->transactionRepository->findByAccounting($accounting); + + foreach ($transactions as $transaction) { + if ($transaction->getTarget() === $accounting) { + $balance = $this->money->add($transaction->getMoney(), $balance); + } + + if ($transaction->getOrigin() === $accounting) { + $balance = $this->money->substract($transaction->getMoney(), $balance); + } + } + + return $balance; + } +} 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..e872582 --- /dev/null +++ b/src/Mapping/Gateway/CheckoutMapper.php @@ -0,0 +1,69 @@ +id = $entity->getId(); + + $gateway = $this->gatewayLocator->getForCheckout($entity); + $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->setGatewayName($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 @@ + * + * @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 TransactionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Transaction::class); + } + + /** + * @return Transaction[] Transactions originated from or targeting the Accounting + */ + public function findByAccounting(Accounting $accounting): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.origin = :val') + ->orWhere('t.target = :val') + ->setParameter('val', $accounting) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return Transaction[] Transactions originated from the Accounting + */ + public function findByOrigin(Accounting $origin): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.origin = :val') + ->setParameter('val', $origin) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return Transaction[] Transactions targetting the Accounting + */ + public function findByTarget(Accounting $origin): array + { + return $this->createQueryBuilder('t') + ->andWhere('t.target = :val') + ->setParameter('val', $origin) + ->orderBy('t.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + + // /** + // * @return Transaction[] Returns an array of Transaction objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Transaction + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/AccountingTransactionRepository.php b/src/Repository/AccountingTransactionRepository.php deleted file mode 100644 index 674c6a1..0000000 --- a/src/Repository/AccountingTransactionRepository.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * @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) - */ -class AccountingTransactionRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, AccountingTransaction::class); - } - - // /** - // * @return AccountingTransaction[] Returns an array of AccountingTransaction objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('t') - // ->andWhere('t.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('t.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?AccountingTransaction - // { - // return $this->createQueryBuilder('t') - // ->andWhere('t.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } -} 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 050535a..0000000 --- a/src/Repository/GatewayCheckoutRepository.php +++ /dev/null @@ -1,48 +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[] 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/UserPersonalRepository.php b/src/Repository/UserPersonalRepository.php new file mode 100644 index 0000000..e4036e7 --- /dev/null +++ b/src/Repository/UserPersonalRepository.php @@ -0,0 +1,23 @@ + + * + * @method UserPersonal|null find($id, $lockMode = null, $lockVersion = null) + * @method UserPersonal|null findOneBy(array $criteria, array $orderBy = null) + * @method UserPersonal[] findAll() + * @method UserPersonal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserPersonalRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserPersonal::class); + } +} 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..755eee7 --- /dev/null +++ b/src/Repository/WalletStatementRepository.php @@ -0,0 +1,106 @@ + + */ +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() + ; + } + + public function findByTransaction(Transaction $transaction): ?WalletStatement + { + if ($transaction->getId() === null) { + return null; + } + + return $this->createQueryBuilder('w') + ->andWhere('w.transaction = :val') + ->setParameter('val', $transaction) + ->getQuery() + ->getOneOrNullResult() + ; + } + + // /** + // * @return WalletStatement[] Returns an array of WalletStatement objects + // */ + // 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 fb3561e..d9e7a40 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -12,9 +12,8 @@ class AuthService private array $config; public function __construct( - private string $appSecret - ) { - } + private string $appSecret, + ) {} /** * @return array{CORS_ALLOW_ORIGIN: string, SESSION_LIFETIME: int} diff --git a/src/Service/Gateway/CheckoutService.php b/src/Service/Gateway/CheckoutService.php new file mode 100644 index 0000000..0b0cd80 --- /dev/null +++ b/src/Service/Gateway/CheckoutService.php @@ -0,0 +1,107 @@ +router->generate( + GatewaysController::REDIRECT, + [ + 'type' => $type, + 'gateway' => $checkout->getGatewayName(), + 'checkoutId' => $checkout->getId(), + ...$parameters, + ], + RouterInterface::ABSOLUTE_URL + ); + } + + /** + * Generates a full tracking code for a given charge in a checkout. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateTracking(Checkout $checkout, Charge $charge): string + { + return \sprintf( + '%s-%s', + $this->generateCheckoutTracking($checkout), + $this->generateChargeTracking($charge) + ); + } + + /** + * Generates a partial tracking code for any given checkout. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateCheckoutTracking(Checkout $checkout): string + { + return \sprintf( + 'AO%d-CO%d', + $checkout->getOrigin()->getId(), + $checkout->getId() + ); + } + + /** + * Generates a partial tracking code for any given charge. + * + * @return string A tracking code suitable for external gateway data matching + */ + public function generateChargeTracking(Charge $charge): string + { + return \sprintf( + 'CH%d-AT%d', + $charge->getId(), + $charge->getTarget()->getId() + ); + } + + /** + * Updates a successful Checkout and generates the transactions for each charge. + * + * @param Checkout $checkout The Checkout to be updated + * + * @return Checkout The updated Checkout with updated charges and generated transactions + */ + public function chargeCheckout(Checkout $checkout): Checkout + { + $checkout->setStatus(CheckoutStatus::Charged); + + foreach ($checkout->getCharges() as $charge) { + $transaction = new Transaction(); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $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..b11ae0c --- /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->get($data->gateway->name)->process($entity); + $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + + return $this->checkoutMapper->toResource($entity); + } +} diff --git a/src/State/Gateway/CheckoutStateProvider.php b/src/State/Gateway/CheckoutStateProvider.php new file mode 100644 index 0000000..0d2d3b9 --- /dev/null +++ b/src/State/Gateway/CheckoutStateProvider.php @@ -0,0 +1,44 @@ +collectionProvider->provide($operation, $uriVariables, $context); + + $resources = []; + foreach ($entities as $entity) { + $resources[] = $this->checkoutMapper->toResource($entity); + } + + return $resources; + } + + $entity = $this->itemProvider->provide($operation, $uriVariables, $context); + + if ($entity === null) { + return null; + } + + return $this->checkoutMapper->toResource($entity); + } +} diff --git a/src/State/GatewayStateProvider.php b/src/State/Gateway/GatewayStateProvider.php similarity index 66% rename from src/State/GatewayStateProvider.php rename to src/State/Gateway/GatewayStateProvider.php index ce91eeb..4c3e477 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 GatewayLocator $gateways, + 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->getAll() as $gateway) { + $gateways[] = $this->gatewayMapper->toResource($gateway); + } + + return $gateways; + } + + private function getGateway(string $name): Gateway + { + try { + $gateway = $this->gateways->get($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 c5e64ea..0000000 --- a/src/State/GatewayCheckoutProcessor.php +++ /dev/null @@ -1,36 +0,0 @@ -gatewayLocator->getGatewayOf($data); - - $this->entityManager->persist($data); - $this->entityManager->flush(); - - $checkout = $gateway->sendData($data); - - $this->entityManager->persist($checkout); - $this->entityManager->flush(); - - return $checkout; - } -} diff --git a/src/State/ResourceVersionStateProvider.php b/src/State/ResourceVersionStateProvider.php index a7482df..3698d73 100644 --- a/src/State/ResourceVersionStateProvider.php +++ b/src/State/ResourceVersionStateProvider.php @@ -18,7 +18,7 @@ class ResourceVersionStateProvider implements ProviderInterface private LogEntryRepository $versionRepository; public function __construct( - private EntityManagerInterface $entityManager + private EntityManagerInterface $entityManager, ) { $this->versionRepository = $this->entityManager->getRepository(LogEntry::class); } diff --git a/src/State/UserStateProcessor.php b/src/State/UserStateProcessor.php index 00f2086..f9d1452 100644 --- a/src/State/UserStateProcessor.php +++ b/src/State/UserStateProcessor.php @@ -13,9 +13,8 @@ class UserStateProcessor implements ProcessorInterface { public function __construct( private ProcessorInterface $innerProcessor, - private UserPasswordHasherInterface $userPasswordHasher - ) { - } + private UserPasswordHasherInterface $userPasswordHasher, + ) {} /** * @return T2 diff --git a/src/State/UserTokenLoginProcessor.php b/src/State/UserTokenLoginProcessor.php index ac3f4e5..b3aa54b 100644 --- a/src/State/UserTokenLoginProcessor.php +++ b/src/State/UserTokenLoginProcessor.php @@ -20,9 +20,8 @@ class UserTokenLoginProcessor implements ProcessorInterface private AuthService $authService, private UserRepository $userRepository, private EntityManagerInterface $entityManager, - private UserPasswordHasherInterface $userPasswordHasher - ) { - } + 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 @@ + $value + * @param SupportedChargeTypes $constraint + */ + public function validate(mixed $value, Constraint $constraint): void + { + if ($value === null || $value === '') { + return; + } + + if (!$value instanceof Collection) { + return; + } + + $checkout = $value->toArray()[0]->getCheckout(); + $gateway = $this->gatewayLocator->getForCheckout($checkout); + $supportedTypes = $gateway->getSupportedChargeTypes(); + + foreach ($value as $charge) { + if (!\in_array($charge->getType(), $supportedTypes)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $charge->getType()->value) + ->setParameter('{{ gateway }}', $gateway->getName()) + ->addViolation(); + } + } + } +} diff --git a/symfony.lock b/symfony.lock index 8fa9d81..d601f29 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,15 @@ "src/ApiResource/.gitignore" ] }, + "doctrine/annotations": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, "doctrine/doctrine-bundle": { "version": "2.12", "recipe": { @@ -40,6 +49,9 @@ "migrations/.gitignore" ] }, + "doctrineencryptbundle/doctrine-encrypt-bundle": { + "version": "5.4.0" + }, "friendsofphp/php-cs-fixer": { "version": "3.58", "recipe": { diff --git a/tests/Entity/Accounting/AccountingTest.php b/tests/Entity/Accounting/AccountingTest.php new file mode 100644 index 0000000..2792f4a --- /dev/null +++ b/tests/Entity/Accounting/AccountingTest.php @@ -0,0 +1,76 @@ +entityManager = static::getContainer()->get(EntityManagerInterface::class); + } + + public function testAccountingGetsUpdatedByCascade() + { + $tipjarA = new Tipjar(); + $tipjarA->setName('TEST_TIPJAR_A'); + + $accountingA = new Accounting(); + $accountingA->setOwner($tipjarA); + + $this->entityManager->persist($tipjarA); + $this->entityManager->flush(); + + /** @var Accounting */ + $accountingA = $this->entityManager->getRepository(Tipjar::class) + ->findOneBy(['name' => 'TEST_TIPJAR_A']) + ->getAccounting(); + + $this->assertNotNull($accountingA->getId()); + + $this->assertSame($tipjarA, $accountingA->getOwner()); + $this->assertSame($tipjarA, $accountingA->getTipjar()); + $this->assertEquals($tipjarA->getId(), $accountingA->getOwner()->getId()); + + $this->assertEquals(null, $accountingA->getUser()); + $this->assertEquals(null, $accountingA->getProject()); + + $tipjarB = new Tipjar(); + $tipjarB->setName('TEST_TIPJAR_B'); + + $accountingB = new Accounting(); + $accountingB->setOwner($tipjarB); + + $this->entityManager->persist($tipjarB); + $this->entityManager->flush(); + + /** @var Accounting */ + $accountingB = $this->entityManager->getRepository(Tipjar::class) + ->findOneBy(['name' => 'TEST_TIPJAR_B']) + ->getAccounting(); + + $this->assertNotNull($accountingB->getId()); + + $this->assertSame($tipjarB, $accountingB->getOwner()); + $this->assertNotSame($tipjarA, $accountingB->getOwner()); + $this->assertSame($tipjarB, $accountingB->getTipjar()); + $this->assertNotSame($tipjarA, $accountingB->getTipjar()); + + $this->assertEquals($tipjarB->getId(), $accountingB->getOwner()->getId()); + $this->assertNotEquals($tipjarA->getId(), $accountingB->getOwner()->getId()); + + $this->assertEquals(null, $accountingB->getUser()); + $this->assertEquals(null, $accountingB->getProject()); + } +} diff --git a/tests/Entity/AccountingTest.php b/tests/Entity/AccountingTest.php deleted file mode 100644 index d0e6191..0000000 --- a/tests/Entity/AccountingTest.php +++ /dev/null @@ -1,136 +0,0 @@ -entityManager = static::getContainer()->get(EntityManagerInterface::class); - } - - public function testAccountingGetsUpdatedByPersistingTransactions() - { - $tipjarA = new Tipjar(); - $tipjarA->setName('TEST_TIPJAR_A'); - - $accA = new Accounting(); - $accA->setTipjar($tipjarA); - - $tipjarB = new Tipjar(); - $tipjarB->setName('TEST_TIPJAR_B'); - - $accB = new Accounting(); - $accB->setTipjar($tipjarB); - - $trx = new AccountingTransaction(); - $trx->setMoney(new Money(120, 'EUR')); - $trx->setOrigin($accA); - $trx->setTarget($accB); - - $this->entityManager->persist($trx); - $this->entityManager->flush(); - - /** @var Accounting */ - $accA = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_A']) - ->getAccounting(); - - /** @var Accounting */ - $accB = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_B']) - ->getAccounting(); - - $this->assertNotNull($accA->getId()); - $this->assertNotNull($accB->getId()); - - $this->assertCount(1, $accA->getTransactionsIssued()); - $this->assertCount(0, $accA->getTransactionsReceived()); - - $this->assertCount(0, $accB->getTransactionsIssued()); - $this->assertCount(1, $accB->getTransactionsReceived()); - - $this->assertEquals(120, $accA->getTransactionsIssued()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accA->getTransactionsIssued()[0]->getMoney()->currency); - - $this->assertEquals(120, $accB->getTransactionsReceived()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accB->getTransactionsReceived()[0]->getMoney()->currency); - } - - public function testAccountingSortsMixedTransactions() - { - $tipjarA = new Tipjar(); - $tipjarA->setName('TEST_TIPJAR_A'); - - $accA = new Accounting(); - $accA->setTipjar($tipjarA); - - $tipjarB = new Tipjar(); - $tipjarB->setName('TEST_TIPJAR_B'); - - $accB = new Accounting(); - $accB->setTipjar($tipjarB); - - $trx1 = new AccountingTransaction(); - $trx1->setMoney(new Money(1, 'EUR')); - $trx1->setOrigin($accA); - $trx1->setTarget($accB); - - $trx2 = new AccountingTransaction(); - $trx2->setMoney(new Money(2, 'USD')); - $trx2->setOrigin($accB); - $trx2->setTarget($accA); - - $this->entityManager->persist($trx1); - $this->entityManager->persist($trx2); - $this->entityManager->flush(); - - /** @var Accounting */ - $accA = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_A']) - ->getAccounting(); - - /** @var Accounting */ - $accB = $this->entityManager->getRepository(Tipjar::class) - ->findOneBy(['name' => 'TEST_TIPJAR_B']) - ->getAccounting(); - - $this->assertNotNull($accA->getId()); - $this->assertNotNull($accB->getId()); - - $this->assertCount(2, $accA->getTransactions()); - $this->assertCount(2, $accB->getTransactions()); - - $this->assertEquals(1, $accA->getTransactions()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accA->getTransactions()[0]->getMoney()->currency); - - $this->assertEquals(1, $accB->getTransactions()[0]->getMoney()->amount); - $this->assertEquals('EUR', $accB->getTransactions()[0]->getMoney()->currency); - - $this->assertEquals(2, $accA->getTransactions()[1]->getMoney()->amount); - $this->assertEquals('USD', $accA->getTransactions()[1]->getMoney()->currency); - - $this->assertEquals(2, $accB->getTransactions()[1]->getMoney()->amount); - $this->assertEquals('USD', $accB->getTransactions()[1]->getMoney()->currency); - - $this->assertEquals($accA, $accA->getTransactions()[0]->getOrigin()); - $this->assertEquals($accA, $accA->getTransactions()[1]->getTarget()); - - $this->assertEquals($accB, $accB->getTransactions()[1]->getOrigin()); - $this->assertEquals($accB, $accB->getTransactions()[0]->getTarget()); - } -} diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index e5579be..ab1978f 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -3,6 +3,10 @@ namespace App\Tests\Entity; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use App\Entity\Project; +use App\Entity\ProjectStatus; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Test\ResetDatabase; @@ -10,25 +14,64 @@ class ProjectApiTest extends ApiTestCase { use ResetDatabase; + private EntityManagerInterface $entityManager; + + public function setUp(): void + { + self::bootKernel(); + + $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); + } + public function testGetCollection(): void { static::createClient()->request('GET', '/v4/projects'); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['@id' => '/v4/projects']); + $this->assertJsonContains(['@type' => 'hydra:Collection']); + $this->assertJsonContains(['hydra:totalItems' => 0]); $this->assertJsonContains(['hydra:member' => []]); + + $owner = new User(); + $owner->setUsername('test_user'); + $owner->setEmail('testuser@example.com'); + $owner->setPassword('projectapitestuserpassword'); + + $project = new Project(); + $project->setTitle('Test Project'); + $project->setOwner($owner); + $project->setStatus(ProjectStatus::Editing); + + $this->entityManager->persist($owner); + $this->entityManager->persist($project); + $this->entityManager->flush(); + + static::createClient()->request('GET', '/v4/projects'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['hydra:totalItems' => 1]); + $this->assertJsonContains(['hydra:member' => [ + [ + 'title' => 'Test Project', + 'accounting' => [], + 'status' => ProjectStatus::Editing->value, + ], + ]]); } - public function testPostUnauthorized(): void + public function testPostUnauthorized() { $client = static::createClient(); $client->request( 'POST', '/v4/projects', - ['json' => [ - 'title' => 'ProjectApiTest Project', - ]] + [ + 'json' => [ + 'title' => 'ProjectApiTest Project', + ], + ] ); $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); diff --git a/tests/Gateway/Paypal/PaypalServiceTest.php b/tests/Gateway/Paypal/PaypalServiceTest.php new file mode 100644 index 0000000..afbbec9 --- /dev/null +++ b/tests/Gateway/Paypal/PaypalServiceTest.php @@ -0,0 +1,46 @@ +paypalService = static::getContainer()->get(PaypalService::class); + } + + public function testAuthenticates(): void + { + $expectedResponseData = ['token_type' => 'Bearer', 'expires_in' => 0, 'access_token' => '']; + $mockResponse = new MockResponse(json_encode($expectedResponseData), [ + 'http_code' => 201, + 'response_headers' => ['Content-Type: application/json'], + ]); + + $httpClient = new MockHttpClient($mockResponse, 'https://testapi.paypal.com'); + $this->paypalService = $this->paypalService->setHttpClient($httpClient); + + $responseData = $this->paypalService->generateAuthToken(); + + $this->assertSame('POST', $mockResponse->getRequestMethod()); + $this->assertSame('https://testapi.paypal.com/v1/oauth2/token', $mockResponse->getRequestUrl()); + + $this->assertArrayHasKey('token_type', $responseData); + $this->assertSame($expectedResponseData['token_type'], $responseData['token_type']); + + $this->assertArrayHasKey('expires_in', $responseData); + $this->assertIsInt($responseData['expires_in']); + + $this->assertArrayHasKey('access_token', $responseData); + $this->assertIsString($responseData['access_token']); + } +} diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php new file mode 100644 index 0000000..72255bc --- /dev/null +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -0,0 +1,315 @@ +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 testSaveAddFunds() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(0, $statements); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(0, $balance->amount); + $this->assertEquals($user->getCurrency(), $balance->currency); + + $incoming = new Transaction(); + $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setOrigin($tipjar); + $incoming->setTarget($user); + + $income = $this->walletService->save($incoming); + + $this->assertEquals(100, $income->getBalance()->amount); + $this->assertEquals(StatementDirection::Incoming->value, $income->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $income->getFinancesTo()); + $this->assertCount(0, $income->getFinancedBy()); + + $this->entityManager->persist($income); + $this->entityManager->flush(); + + $balance = $this->walletService->getBalance($user); + + $this->assertEquals(100, $balance->amount); + $this->assertEquals('EUR', $balance->currency); + + $statements = $this->walletService->getStatements($user); + + $this->assertCount(1, $statements); + + $statement = $statements[0]; + $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); + $this->assertEquals('EUR', $balance->currency); + $this->assertCount(0, $statement->getFinancesTo()); + $this->assertCount(0, $statement->getFinancedBy()); + } + + public function testTransactionsListenerAddFunds() + { + $tipjar = $this->getTipjar()->getAccounting(); + $user = $this->getUser()->getAccounting(); + + $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->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 testSpendFinancesExpenses() + { + $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); + + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); + + $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); + + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); + + $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); + + $expenditure = $this->walletService->spend($outgoing); + + $this->entityManager->persist($expenditure); + $this->entityManager->flush(); + + $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/Benzina/Pump/UsersPumpTest.php b/tests/Library/Benzina/Pump/UsersPumpTest.php index 6b7d32e..b4afbb8 100644 --- a/tests/Library/Benzina/Pump/UsersPumpTest.php +++ b/tests/Library/Benzina/Pump/UsersPumpTest.php @@ -95,7 +95,7 @@ class UsersPumpTest extends KernelTestCase $this->assertCount(0, $usersPrePumping); - $this->pump->process([$testUser]); + $this->pump->pump([$testUser]); $usersPostPumping = $this->entityManager->getRepository(User::class) ->findAll(); @@ -108,6 +108,6 @@ class UsersPumpTest extends KernelTestCase $this->assertNotEquals($testUser['confirmed'], $user->isEmailConfirmed()); $this->assertTrue($user->isMigrated()); - $this->assertEquals($testUser['id'], $user->getMigratedReference()); + $this->assertEquals($testUser['id'], $user->getMigratedId()); } } diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Library/Economy/MoneyServiceTest.php new file mode 100644 index 0000000..2ad6b97 --- /dev/null +++ b/tests/Library/Economy/MoneyServiceTest.php @@ -0,0 +1,102 @@ +moneyService = static::getContainer()->get(MoneyService::class); + } + + public function testComparesLess() + { + $more = new Money(300, 'EUR'); + $less = new Money(200, 'EUR'); + + $this->assertTrue($this->moneyService->isLess($less, $more)); + $this->assertFalse($this->moneyService->isLess($more, $less)); + + // We are cooked if this fails and I'm not talking about code + $this->assertTrue($this->moneyService->isLess(new Money(100, 'JPY'), $more)); + } + + 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)); + } + + public function testAddition() + { + $a = new Money(100, 'EUR'); + $b = new Money(110, 'EUR'); + + $c = $this->moneyService->add($a, $b); + + $this->assertEquals(100, $a->amount); + $this->assertEquals(110, $b->amount); + $this->assertEquals(210, $c->amount); + + $d = new Money(100, 'USD'); + $e = $this->moneyService->add($a, $d); + + $this->assertEquals($d->amount, $a->amount); + $this->assertEquals($d->currency, $e->currency); + + $this->assertNotEquals($d->amount, $e->amount); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $d)); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); + } + + public function testSubstraction() + { + $a = new Money(101, 'EUR'); + $b = new Money(210, 'EUR'); + + $c = $this->moneyService->substract($a, $b); + + $this->assertEquals(101, $a->amount); + $this->assertEquals(210, $b->amount); + $this->assertEquals(109, $c->amount); + + $d = new Money(500, 'USD'); + $e = $this->moneyService->substract($a, $d); + + $this->assertEquals($d->currency, $e->currency); + + $this->assertNotEquals($d->amount, $e->amount); + $this->assertTrue($this->moneyService->isLess($e, $d)); + $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); + } +}